ai removed boilerplate and orgnized code even more

This commit is contained in:
androidlover5842
2026-02-02 07:00:56 +05:30
parent d54a9af5ee
commit a691e84fd8
28 changed files with 1077 additions and 1038 deletions

View File

@@ -0,0 +1,25 @@
package com.android.trisolarispms.core.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
internal fun <S> ViewModel.launchRequest(
state: MutableStateFlow<S>,
setLoading: (S) -> S,
setError: (S, String) -> S,
defaultError: String,
block: suspend () -> Unit
) {
viewModelScope.launch {
state.update(setLoading)
try {
block()
} catch (e: Exception) {
val message = e.localizedMessage ?: defaultError
state.update { current -> setError(current, message) }
}
}
}

View File

@@ -1,12 +1,11 @@
package com.android.trisolarispms.ui.guest
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.core.viewmodel.launchRequest
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.toRequestBody
@@ -21,9 +20,12 @@ class GuestSignatureViewModel : ViewModel() {
fun uploadSignature(propertyId: String, guestId: String, svg: String, onDone: () -> Unit) {
if (propertyId.isBlank() || guestId.isBlank()) return
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) }
try {
launchRequest(
state = _state,
setLoading = { it.copy(isLoading = true, error = null) },
setError = { current, message -> current.copy(isLoading = false, error = message) },
defaultError = "Upload failed"
) {
val api = ApiClient.create()
val requestBody = svg.toRequestBody("image/svg+xml".toMediaType())
val part = MultipartBody.Part.createFormData(
@@ -38,9 +40,6 @@ class GuestSignatureViewModel : ViewModel() {
} else {
_state.update { it.copy(isLoading = false, error = "Upload failed: ${response.code()}") }
}
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Upload failed") }
}
}
}
}

View File

@@ -1,15 +1,14 @@
package com.android.trisolarispms.ui.home
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.core.auth.Role
import com.android.trisolarispms.core.auth.toRoles
import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.data.api.model.PropertyAccessCodeJoinRequest
import com.android.trisolarispms.core.viewmodel.launchRequest
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
data class HomeJoinPropertyState(
val isLoading: Boolean = false,
@@ -34,9 +33,12 @@ class HomeJoinPropertyViewModel : ViewModel() {
_state.update { it.copy(error = "Code must be 6 digits", message = null) }
return
}
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null, message = null) }
try {
launchRequest(
state = _state,
setLoading = { it.copy(isLoading = true, error = null, message = null) },
setError = { current, message -> current.copy(isLoading = false, error = message, message = null) },
defaultError = "Join failed"
) {
val response = ApiClient.create().joinAccessCode(
PropertyAccessCodeJoinRequest(
propertyId = trimmedPropertyId,
@@ -57,9 +59,6 @@ class HomeJoinPropertyViewModel : ViewModel() {
} else {
_state.update { it.copy(isLoading = false, error = "Join failed: ${response.code()}") }
}
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Join failed") }
}
}
}

View File

@@ -8,6 +8,7 @@ import com.android.trisolarispms.ui.auth.AuthViewModel
import com.android.trisolarispms.ui.razorpay.RazorpayQrScreen
import com.android.trisolarispms.ui.razorpay.RazorpaySettingsScreen
import com.android.trisolarispms.ui.roomstay.ActiveRoomStaysScreen
import com.android.trisolarispms.ui.roomstay.ManageRoomStaySelection
import com.android.trisolarispms.ui.roomstay.ManageRoomStayRatesScreen
import com.android.trisolarispms.ui.roomstay.ManageRoomStaySelectScreen
@@ -18,23 +19,56 @@ internal fun renderStayFlowRoutes(
authViewModel: AuthViewModel,
authz: AuthzPolicy
): Boolean {
@Composable
fun renderManageRoomStaySelectRoute(
propertyId: String,
fromAt: String,
toAt: String?,
onNext: (List<ManageRoomStaySelection>) -> Unit
) {
ManageRoomStaySelectScreen(
propertyId = propertyId,
bookingFromAt = fromAt,
bookingToAt = toAt,
onBack = { refs.openActiveRoomStays(propertyId) },
onNext = { rooms ->
refs.selectedManageRooms.value = rooms
onNext(rooms)
}
)
}
@Composable
fun renderManageRoomStayRatesRoute(
propertyId: String,
bookingId: String,
fromAt: String,
toAt: String?,
onBack: () -> Unit,
onDone: () -> Unit
) {
ManageRoomStayRatesScreen(
propertyId = propertyId,
bookingId = bookingId,
checkInAt = fromAt,
checkOutAt = toAt,
selectedRooms = refs.selectedManageRooms.value,
onBack = onBack,
onDone = onDone
)
}
when (val currentRoute = refs.currentRoute) {
is AppRoute.ActiveRoomStays -> ActiveRoomStaysScreen(
propertyId = currentRoute.propertyId,
propertyName = currentRoute.propertyName,
onBack = {
val blockBack = authz.shouldBlockBackToHome(
propertyId = currentRoute.propertyId,
propertyCount = state.propertyRoles.size
)
val blockBack = shouldBlockHomeBack(authz, state, currentRoute.propertyId)
if (!blockBack) {
refs.route.value = AppRoute.Home
}
},
showBack = !authz.shouldBlockBackToHome(
propertyId = currentRoute.propertyId,
propertyCount = state.propertyRoles.size
),
showBack = !shouldBlockHomeBack(authz, state, currentRoute.propertyId),
onViewRooms = { refs.route.value = AppRoute.Rooms(currentRoute.propertyId) },
onCreateBooking = { refs.route.value = AppRoute.CreateBooking(currentRoute.propertyId) },
canCreateBooking = authz.canCreateBookingFor(currentRoute.propertyId),
@@ -91,13 +125,11 @@ internal fun renderStayFlowRoutes(
}
)
is AppRoute.ManageRoomStaySelect -> ManageRoomStaySelectScreen(
is AppRoute.ManageRoomStaySelect -> renderManageRoomStaySelectRoute(
propertyId = currentRoute.propertyId,
bookingFromAt = currentRoute.fromAt,
bookingToAt = currentRoute.toAt,
onBack = { refs.openActiveRoomStays(currentRoute.propertyId) },
onNext = { rooms ->
refs.selectedManageRooms.value = rooms
fromAt = currentRoute.fromAt,
toAt = currentRoute.toAt
) {
refs.route.value = AppRoute.ManageRoomStayRates(
propertyId = currentRoute.propertyId,
bookingId = currentRoute.bookingId,
@@ -105,15 +137,12 @@ internal fun renderStayFlowRoutes(
toAt = currentRoute.toAt
)
}
)
is AppRoute.ManageRoomStaySelectFromBooking -> ManageRoomStaySelectScreen(
is AppRoute.ManageRoomStaySelectFromBooking -> renderManageRoomStaySelectRoute(
propertyId = currentRoute.propertyId,
bookingFromAt = currentRoute.fromAt,
bookingToAt = currentRoute.toAt,
onBack = { refs.openActiveRoomStays(currentRoute.propertyId) },
onNext = { rooms ->
refs.selectedManageRooms.value = rooms
fromAt = currentRoute.fromAt,
toAt = currentRoute.toAt
) {
refs.route.value = AppRoute.ManageRoomStayRatesFromBooking(
propertyId = currentRoute.propertyId,
bookingId = currentRoute.bookingId,
@@ -122,31 +151,28 @@ internal fun renderStayFlowRoutes(
toAt = currentRoute.toAt
)
}
)
is AppRoute.ManageRoomStayRates -> ManageRoomStayRatesScreen(
is AppRoute.ManageRoomStayRates -> renderManageRoomStayRatesRoute(
propertyId = currentRoute.propertyId,
bookingId = currentRoute.bookingId,
checkInAt = currentRoute.fromAt,
checkOutAt = currentRoute.toAt,
selectedRooms = refs.selectedManageRooms.value,
fromAt = currentRoute.fromAt,
toAt = currentRoute.toAt,
onBack = {
refs.route.value = AppRoute.ManageRoomStaySelect(
currentRoute.propertyId,
currentRoute.bookingId,
currentRoute.fromAt,
currentRoute.toAt
propertyId = currentRoute.propertyId,
bookingId = currentRoute.bookingId,
fromAt = currentRoute.fromAt,
toAt = currentRoute.toAt
)
},
onDone = { refs.openActiveRoomStays(currentRoute.propertyId) }
)
is AppRoute.ManageRoomStayRatesFromBooking -> ManageRoomStayRatesScreen(
is AppRoute.ManageRoomStayRatesFromBooking -> renderManageRoomStayRatesRoute(
propertyId = currentRoute.propertyId,
bookingId = currentRoute.bookingId,
checkInAt = currentRoute.fromAt,
checkOutAt = currentRoute.toAt,
selectedRooms = refs.selectedManageRooms.value,
fromAt = currentRoute.fromAt,
toAt = currentRoute.toAt,
onBack = {
refs.route.value = AppRoute.ManageRoomStaySelectFromBooking(
currentRoute.propertyId,
@@ -169,3 +195,9 @@ internal fun renderStayFlowRoutes(
}
return true
}
private fun shouldBlockHomeBack(authz: AuthzPolicy, state: AuthUiState, propertyId: String): Boolean =
authz.shouldBlockBackToHome(
propertyId = propertyId,
propertyCount = state.propertyRoles.size
)

View File

@@ -3,21 +3,20 @@ package com.android.trisolarispms.ui.payment
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.data.api.core.ApiService
import com.android.trisolarispms.data.api.model.RazorpayRefundRequest
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import retrofit2.Response
class BookingPaymentsViewModel : ViewModel() {
private val _state = MutableStateFlow(BookingPaymentsState())
val state: StateFlow<BookingPaymentsState> = _state
fun load(propertyId: String, bookingId: String) {
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null, message = null) }
try {
val api = ApiClient.create()
runPaymentAction(defaultError = "Load failed") { api ->
val response = api.listPayments(propertyId, bookingId)
val body = response.body()
if (response.isSuccessful && body != null) {
@@ -30,22 +29,7 @@ class BookingPaymentsViewModel : ViewModel() {
)
}
} else {
_state.update {
it.copy(
isLoading = false,
error = "Load failed: ${response.code()}",
message = null
)
}
}
} catch (e: Exception) {
_state.update {
it.copy(
isLoading = false,
error = e.localizedMessage ?: "Load failed",
message = null
)
}
setActionFailure("Load", response)
}
}
}
@@ -55,10 +39,7 @@ class BookingPaymentsViewModel : ViewModel() {
_state.update { it.copy(error = "Amount must be greater than 0", message = null) }
return
}
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null, message = null) }
try {
val api = ApiClient.create()
runPaymentAction(defaultError = "Create failed") { api ->
val response = api.createPayment(
propertyId = propertyId,
bookingId = bookingId,
@@ -75,31 +56,13 @@ class BookingPaymentsViewModel : ViewModel() {
)
}
} else {
_state.update {
it.copy(
isLoading = false,
error = "Create failed: ${response.code()}",
message = null
)
}
}
} catch (e: Exception) {
_state.update {
it.copy(
isLoading = false,
error = e.localizedMessage ?: "Create failed",
message = null
)
}
setActionFailure("Create", response)
}
}
}
fun deleteCashPayment(propertyId: String, bookingId: String, paymentId: String) {
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null, message = null) }
try {
val api = ApiClient.create()
runPaymentAction(defaultError = "Delete failed") { api ->
val response = api.deletePayment(
propertyId = propertyId,
bookingId = bookingId,
@@ -115,22 +78,7 @@ class BookingPaymentsViewModel : ViewModel() {
)
}
} else {
_state.update {
it.copy(
isLoading = false,
error = "Delete failed: ${response.code()}",
message = null
)
}
}
} catch (e: Exception) {
_state.update {
it.copy(
isLoading = false,
error = e.localizedMessage ?: "Delete failed",
message = null
)
}
setActionFailure("Delete", response)
}
}
}
@@ -151,10 +99,7 @@ class BookingPaymentsViewModel : ViewModel() {
_state.update { it.copy(error = "Missing payment ID", message = null) }
return
}
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null, message = null) }
try {
val api = ApiClient.create()
runPaymentAction(defaultError = "Refund failed") { api ->
val response = api.refundRazorpayPayment(
propertyId = propertyId,
bookingId = bookingId,
@@ -176,23 +121,38 @@ class BookingPaymentsViewModel : ViewModel() {
}
load(propertyId, bookingId)
} else {
_state.update {
it.copy(
isLoading = false,
error = "Refund failed: ${response.code()}",
message = null
)
setActionFailure("Refund", response)
}
}
}
private fun runPaymentAction(
defaultError: String,
block: suspend (ApiService) -> Unit
) {
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null, message = null) }
try {
block(ApiClient.create())
} catch (e: Exception) {
_state.update {
it.copy(
isLoading = false,
error = e.localizedMessage ?: "Refund failed",
error = e.localizedMessage ?: defaultError,
message = null
)
}
}
}
}
private fun setActionFailure(action: String, response: Response<*>) {
_state.update {
it.copy(
isLoading = false,
error = "$action failed: ${response.code()}",
message = null
)
}
}
}

View File

@@ -1,13 +1,12 @@
package com.android.trisolarispms.ui.property
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.data.api.model.PropertyCreateRequest
import com.android.trisolarispms.core.viewmodel.launchRequest
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
class AddPropertyViewModel : ViewModel() {
private val _state = MutableStateFlow(AddPropertyState())
@@ -56,9 +55,12 @@ class AddPropertyViewModel : ViewModel() {
return
}
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) }
try {
launchRequest(
state = _state,
setLoading = { it.copy(isLoading = true, error = null) },
setError = { current, message -> current.copy(isLoading = false, error = message) },
defaultError = "Create failed"
) {
val api = ApiClient.create()
val body = PropertyCreateRequest(
code = null,
@@ -79,9 +81,6 @@ class AddPropertyViewModel : ViewModel() {
} else {
_state.update { it.copy(isLoading = false, error = "Create failed: ${response.code()}") }
}
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Create failed") }
}
}
}

View File

@@ -1,21 +1,23 @@
package com.android.trisolarispms.ui.property
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.core.viewmodel.launchRequest
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
class PropertyListViewModel : ViewModel() {
private val _state = MutableStateFlow(PropertyListState())
val state: StateFlow<PropertyListState> = _state
fun refresh() {
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) }
try {
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.listProperties()
if (response.isSuccessful) {
@@ -29,9 +31,6 @@ class PropertyListViewModel : ViewModel() {
} else {
_state.update { it.copy(isLoading = false, error = "Load failed: ${response.code()}") }
}
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Load failed") }
}
}
}
}

View File

@@ -3,12 +3,14 @@ package com.android.trisolarispms.ui.room
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.data.api.core.ApiService
import com.android.trisolarispms.data.api.model.RoomCreateRequest
import com.android.trisolarispms.data.api.model.RoomUpdateRequest
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import retrofit2.Response
class RoomFormViewModel : ViewModel() {
private val _state = MutableStateFlow(RoomFormState())
@@ -58,70 +60,22 @@ class RoomFormViewModel : ViewModel() {
fun onNotesChange(value: String) = _state.update { it.copy(notes = value, error = null) }
fun submitCreate(propertyId: String, onDone: () -> Unit) {
val roomNumberText = state.value.roomNumber.trim()
val roomTypeCode = state.value.roomTypeCode.trim()
val roomNumber = roomNumberText.toIntOrNull()
if (roomNumber == null || roomTypeCode.isBlank()) {
_state.update { it.copy(error = "Room number must be a number and room type is required") }
return
}
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) }
try {
val api = ApiClient.create()
val body = RoomCreateRequest(
roomNumber = roomNumber,
floor = state.value.floor.trim().toIntOrNull(),
roomTypeCode = roomTypeCode,
hasNfc = state.value.hasNfc,
active = state.value.active,
maintenance = state.value.maintenance,
notes = state.value.notes.takeIf { it.isNotBlank() }
)
val response = api.createRoom(propertyId, body)
if (response.isSuccessful) {
_state.update { it.copy(isLoading = false, success = true) }
onDone()
} else {
_state.update { it.copy(isLoading = false, error = "Create failed: ${response.code()}") }
}
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Create failed") }
}
val input = readValidatedInput() ?: return
submitMutation(
action = "Create",
onDone = onDone
) { api ->
api.createRoom(propertyId, input.toCreateRequest())
}
}
fun submitUpdate(propertyId: String, roomId: String, onDone: () -> Unit) {
val roomNumberText = state.value.roomNumber.trim()
val roomTypeCode = state.value.roomTypeCode.trim()
val roomNumber = roomNumberText.toIntOrNull()
if (roomNumber == null || roomTypeCode.isBlank()) {
_state.update { it.copy(error = "Room number must be a number and room type is required") }
return
}
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) }
try {
val api = ApiClient.create()
val body = RoomUpdateRequest(
roomNumber = roomNumber,
floor = state.value.floor.trim().toIntOrNull(),
roomTypeCode = roomTypeCode,
hasNfc = state.value.hasNfc,
active = state.value.active,
maintenance = state.value.maintenance,
notes = state.value.notes.takeIf { it.isNotBlank() }
)
val response = api.updateRoom(propertyId, roomId, body)
if (response.isSuccessful) {
_state.update { it.copy(isLoading = false, success = true) }
onDone()
} else {
_state.update { it.copy(isLoading = false, error = "Update failed: ${response.code()}") }
}
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Update failed") }
}
val input = readValidatedInput() ?: return
submitMutation(
action = "Update",
onDone = onDone
) { api ->
api.updateRoom(propertyId, roomId, input.toUpdateRequest())
}
}
@@ -130,19 +84,80 @@ class RoomFormViewModel : ViewModel() {
_state.update { it.copy(error = "Room ID is missing") }
return
}
submitMutation(
action = "Delete",
onDone = onDone
) { api ->
api.deleteRoom(propertyId, roomId)
}
}
private data class ValidRoomInput(
val roomNumber: Int,
val floor: Int?,
val roomTypeCode: String,
val hasNfc: Boolean,
val active: Boolean,
val maintenance: Boolean,
val notes: String?
) {
fun toCreateRequest(): RoomCreateRequest = RoomCreateRequest(
roomNumber = roomNumber,
floor = floor,
roomTypeCode = roomTypeCode,
hasNfc = hasNfc,
active = active,
maintenance = maintenance,
notes = notes
)
fun toUpdateRequest(): RoomUpdateRequest = RoomUpdateRequest(
roomNumber = roomNumber,
floor = floor,
roomTypeCode = roomTypeCode,
hasNfc = hasNfc,
active = active,
maintenance = maintenance,
notes = notes
)
}
private fun readValidatedInput(): ValidRoomInput? {
val current = state.value
val roomNumber = current.roomNumber.trim().toIntOrNull()
val roomTypeCode = current.roomTypeCode.trim()
if (roomNumber == null || roomTypeCode.isBlank()) {
_state.update { it.copy(error = "Room number must be a number and room type is required") }
return null
}
return ValidRoomInput(
roomNumber = roomNumber,
floor = current.floor.trim().toIntOrNull(),
roomTypeCode = roomTypeCode,
hasNfc = current.hasNfc,
active = current.active,
maintenance = current.maintenance,
notes = current.notes.takeIf { it.isNotBlank() }
)
}
private fun submitMutation(
action: String,
onDone: () -> Unit,
call: suspend (ApiService) -> Response<*>
) {
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) }
try {
val api = ApiClient.create()
val response = api.deleteRoom(propertyId, roomId)
val response = call(ApiClient.create())
if (response.isSuccessful) {
_state.update { it.copy(isLoading = false, success = true) }
onDone()
} else {
_state.update { it.copy(isLoading = false, error = "Delete failed: ${response.code()}") }
_state.update { it.copy(isLoading = false, error = "$action failed: ${response.code()}") }
}
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Delete failed") }
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "$action failed") }
}
}
}

View File

@@ -1,12 +1,11 @@
package com.android.trisolarispms.ui.room
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.core.viewmodel.launchRequest
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
class RoomListViewModel : ViewModel() {
private val _state = MutableStateFlow(RoomListState())
@@ -14,16 +13,19 @@ class RoomListViewModel : ViewModel() {
fun load(propertyId: String, showAll: Boolean = false, roomTypeCode: String? = null) {
if (propertyId.isBlank()) return
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null, showAll = showAll, roomTypeCode = roomTypeCode) }
try {
launchRequest(
state = _state,
setLoading = { it.copy(isLoading = true, error = null, showAll = showAll, roomTypeCode = roomTypeCode) },
setError = { current, message -> current.copy(isLoading = false, error = message) },
defaultError = "Load failed"
) {
val api = ApiClient.create()
val trimmedCode = roomTypeCode?.trim().orEmpty()
val response = if (trimmedCode.isNotBlank()) {
api.listRoomsByType(
propertyId = propertyId,
roomTypeCode = trimmedCode,
availableOnly = if (showAll) false else true
availableOnly = !showAll
)
} else if (showAll) {
api.listRooms(propertyId)
@@ -41,9 +43,6 @@ class RoomListViewModel : ViewModel() {
} else {
_state.update { it.copy(isLoading = false, error = "Load failed: ${response.code()}") }
}
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Load failed") }
}
}
}

View File

@@ -1,34 +1,12 @@
package com.android.trisolarispms.ui.roomimage
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Done
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
@Composable
@OptIn(ExperimentalMaterial3Api::class)
fun AddImageTagScreen(
onBack: () -> Unit,
onSave: () -> Unit,
@@ -40,41 +18,12 @@ fun AddImageTagScreen(
viewModel.reset()
}
Scaffold(
topBar = {
TopAppBar(
title = { Text("Add Tag") },
navigationIcon = {
IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
}
},
actions = {
IconButton(onClick = { viewModel.submitCreate(onSave) }) {
Icon(Icons.Default.Done, contentDescription = "Save")
}
},
colors = TopAppBarDefaults.topAppBarColors()
ImageTagFormScreen(
title = "Add Tag",
name = state.name,
error = state.error,
onNameChange = viewModel::onNameChange,
onBack = onBack,
onSave = { viewModel.submitCreate(onSave) }
)
}
) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.padding(24.dp),
verticalArrangement = Arrangement.Top
) {
OutlinedTextField(
value = state.name,
onValueChange = viewModel::onNameChange,
label = { Text("Name") },
modifier = Modifier.fillMaxWidth()
)
state.error?.let {
Spacer(modifier = Modifier.height(12.dp))
Text(text = it, color = MaterialTheme.colorScheme.error)
}
}
}
}

View File

@@ -1,35 +1,13 @@
package com.android.trisolarispms.ui.roomimage
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Done
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.android.trisolarispms.data.api.model.RoomImageTagDto
@Composable
@OptIn(ExperimentalMaterial3Api::class)
fun EditImageTagScreen(
tag: RoomImageTagDto,
onBack: () -> Unit,
@@ -42,41 +20,12 @@ fun EditImageTagScreen(
viewModel.setTag(tag)
}
Scaffold(
topBar = {
TopAppBar(
title = { Text("Edit Tag") },
navigationIcon = {
IconButton(onClick = onBack) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
}
},
actions = {
IconButton(onClick = { viewModel.submitUpdate(tag.id.orEmpty(), onSave) }) {
Icon(Icons.Default.Done, contentDescription = "Save")
}
},
colors = TopAppBarDefaults.topAppBarColors()
ImageTagFormScreen(
title = "Edit Tag",
name = state.name,
error = state.error,
onNameChange = viewModel::onNameChange,
onBack = onBack,
onSave = { viewModel.submitUpdate(tag.id.orEmpty(), onSave) }
)
}
) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.padding(24.dp),
verticalArrangement = Arrangement.Top
) {
OutlinedTextField(
value = state.name,
onValueChange = viewModel::onNameChange,
label = { Text("Name") },
modifier = Modifier.fillMaxWidth()
)
state.error?.let {
Spacer(modifier = Modifier.height(12.dp))
Text(text = it, color = MaterialTheme.colorScheme.error)
}
}
}
}

View File

@@ -0,0 +1,73 @@
package com.android.trisolarispms.ui.roomimage
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Done
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
@OptIn(ExperimentalMaterial3Api::class)
internal fun ImageTagFormScreen(
title: String,
name: String,
error: String?,
onNameChange: (String) -> Unit,
onBack: () -> Unit,
onSave: () -> Unit
) {
Scaffold(
topBar = {
TopAppBar(
title = { Text(title) },
navigationIcon = {
IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
}
},
actions = {
IconButton(onClick = onSave) {
Icon(Icons.Default.Done, contentDescription = "Save")
}
},
colors = TopAppBarDefaults.topAppBarColors()
)
}
) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.padding(24.dp),
verticalArrangement = Arrangement.Top
) {
OutlinedTextField(
value = name,
onValueChange = onNameChange,
label = { Text("Name") },
modifier = Modifier.fillMaxWidth()
)
error?.let {
Spacer(modifier = Modifier.height(12.dp))
Text(text = it, color = MaterialTheme.colorScheme.error)
}
}
}
}

View File

@@ -3,11 +3,13 @@ package com.android.trisolarispms.ui.roomimage
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.data.api.core.ApiService
import com.android.trisolarispms.data.api.model.RoomImageTagDto
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import retrofit2.Response
class ImageTagFormViewModel : ViewModel() {
private val _state = MutableStateFlow(ImageTagFormState())
@@ -26,47 +28,57 @@ class ImageTagFormViewModel : ViewModel() {
}
fun submitCreate(onDone: () -> Unit) {
val name = state.value.name.trim()
if (name.isBlank()) {
_state.update { it.copy(error = "Name is required") }
return
}
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) }
try {
val api = ApiClient.create()
val response = api.createImageTag(RoomImageTagDto(name = name))
if (response.isSuccessful) {
_state.update { ImageTagFormState(success = true) }
onDone()
} else {
_state.update { it.copy(isLoading = false, error = "Create failed: ${response.code()}") }
}
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Create failed") }
}
val payload = readValidatedPayload() ?: return
submitMutation(
action = "Create",
onDone = onDone,
resetOnSuccess = true
) { api ->
api.createImageTag(payload)
}
}
fun submitUpdate(tagId: String, onDone: () -> Unit) {
val payload = readValidatedPayload() ?: return
submitMutation(
action = "Update",
onDone = onDone,
resetOnSuccess = false
) { api ->
api.updateImageTag(tagId, payload)
}
}
private fun readValidatedPayload(): RoomImageTagDto? {
val name = state.value.name.trim()
if (name.isBlank()) {
_state.update { it.copy(error = "Name is required") }
return
return null
}
return RoomImageTagDto(name = name)
}
private fun submitMutation(
action: String,
onDone: () -> Unit,
resetOnSuccess: Boolean,
call: suspend (ApiService) -> Response<*>
) {
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) }
try {
val api = ApiClient.create()
val response = api.updateImageTag(tagId, RoomImageTagDto(name = name))
val response = call(ApiClient.create())
if (response.isSuccessful) {
_state.update { it.copy(isLoading = false, success = true) }
_state.update {
if (resetOnSuccess) ImageTagFormState(success = true)
else it.copy(isLoading = false, success = true)
}
onDone()
} else {
_state.update { it.copy(isLoading = false, error = "Update failed: ${response.code()}") }
_state.update { it.copy(isLoading = false, error = "$action failed: ${response.code()}") }
}
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Update failed") }
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "$action failed") }
}
}
}

View File

@@ -1,21 +1,23 @@
package com.android.trisolarispms.ui.roomimage
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.core.viewmodel.launchRequest
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
class ImageTagViewModel : ViewModel() {
private val _state = MutableStateFlow(ImageTagState())
val state: StateFlow<ImageTagState> = _state
fun load() {
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) }
try {
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.listImageTags()
if (response.isSuccessful) {
@@ -29,17 +31,17 @@ class ImageTagViewModel : ViewModel() {
} else {
_state.update { it.copy(isLoading = false, error = "Load failed: ${response.code()}") }
}
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Load failed") }
}
}
}
fun delete(tagId: String) {
if (tagId.isBlank()) return
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) }
try {
launchRequest(
state = _state,
setLoading = { it.copy(isLoading = true, error = null) },
setError = { current, message -> current.copy(isLoading = false, error = message) },
defaultError = "Delete failed"
) {
val api = ApiClient.create()
val response = api.deleteImageTag(tagId)
if (response.isSuccessful) {
@@ -53,9 +55,6 @@ class ImageTagViewModel : ViewModel() {
} else {
_state.update { it.copy(isLoading = false, error = "Delete failed: ${response.code()}") }
}
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Delete failed") }
}
}
}
}

View File

@@ -1,12 +1,11 @@
package com.android.trisolarispms.ui.roomstay
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.core.viewmodel.launchRequest
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
class ActiveRoomStaysViewModel : ViewModel() {
private val _state = MutableStateFlow(ActiveRoomStaysState())
@@ -14,9 +13,12 @@ class ActiveRoomStaysViewModel : ViewModel() {
fun load(propertyId: String) {
if (propertyId.isBlank()) return
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) }
try {
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 activeResponse = api.listActiveRoomStays(propertyId)
val bookingsResponse = api.listBookings(propertyId, status = "CHECKED_IN")
@@ -32,9 +34,6 @@ class ActiveRoomStaysViewModel : ViewModel() {
} else {
_state.update { it.copy(isLoading = false, error = "Load failed: ${activeResponse.code()}") }
}
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Load failed") }
}
}
}
}

View File

@@ -5,13 +5,14 @@ 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.google.gson.Gson
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.delay
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import okhttp3.Request
import okhttp3.sse.EventSource
import okhttp3.sse.EventSourceListener
@@ -30,9 +31,12 @@ class BookingDetailsViewModel : ViewModel() {
fun load(propertyId: String, bookingId: String) {
if (propertyId.isBlank() || bookingId.isBlank()) return
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) }
try {
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) {
@@ -46,9 +50,6 @@ class BookingDetailsViewModel : ViewModel() {
} else {
_state.update { it.copy(isLoading = false, error = "Load failed: ${response.code()}") }
}
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Load failed") }
}
}
}

View File

@@ -1,12 +1,11 @@
package com.android.trisolarispms.ui.roomstay
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.core.viewmodel.launchRequest
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
class BookingRoomStaysViewModel : ViewModel() {
private val _state = MutableStateFlow(BookingRoomStaysState())
@@ -18,9 +17,12 @@ class BookingRoomStaysViewModel : ViewModel() {
fun load(propertyId: String, bookingId: String) {
if (propertyId.isBlank() || bookingId.isBlank()) return
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) }
try {
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.listActiveRoomStays(propertyId)
if (response.isSuccessful) {
@@ -35,9 +37,6 @@ class BookingRoomStaysViewModel : ViewModel() {
} else {
_state.update { it.copy(isLoading = false, error = "Load failed: ${response.code()}") }
}
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Load failed") }
}
}
}
}

View File

@@ -1,14 +1,13 @@
package com.android.trisolarispms.ui.roomstay
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.data.api.model.BookingBulkCheckInRequest
import com.android.trisolarispms.data.api.model.BookingBulkCheckInStayRequest
import com.android.trisolarispms.core.viewmodel.launchRequest
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
class ManageRoomStayRatesViewModel : ViewModel() {
private val _state = MutableStateFlow(ManageRoomStayRatesState())
@@ -79,9 +78,12 @@ class ManageRoomStayRatesViewModel : ViewModel() {
if (propertyId.isBlank() || bookingId.isBlank() || checkInAt.isBlank()) return
val items = _state.value.items
if (items.isEmpty()) return
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) }
try {
launchRequest(
state = _state,
setLoading = { it.copy(isLoading = true, error = null) },
setError = { current, message -> current.copy(isLoading = false, error = message) },
defaultError = "Update failed"
) {
val api = ApiClient.create()
val stays = items.map { item ->
BookingBulkCheckInStayRequest(
@@ -105,9 +107,6 @@ class ManageRoomStayRatesViewModel : ViewModel() {
} else {
_state.update { it.copy(isLoading = false, error = "Update failed: ${response.code()}") }
}
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Update failed") }
}
}
}
}

View File

@@ -1,12 +1,11 @@
package com.android.trisolarispms.ui.roomstay
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.core.viewmodel.launchRequest
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
class ManageRoomStaySelectViewModel : ViewModel() {
private val _state = MutableStateFlow(ManageRoomStaySelectState())
@@ -14,9 +13,12 @@ class ManageRoomStaySelectViewModel : ViewModel() {
fun load(propertyId: String, from: String, to: String) {
if (propertyId.isBlank() || from.isBlank() || to.isBlank()) return
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) }
try {
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.listAvailableRoomsWithRate(propertyId, from = from, to = to)
if (response.isSuccessful) {
@@ -30,9 +32,6 @@ class ManageRoomStaySelectViewModel : ViewModel() {
} else {
_state.update { it.copy(isLoading = false, error = "Load failed: ${response.code()}") }
}
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Load failed") }
}
}
}
}

View File

@@ -10,13 +10,12 @@ fun AddAmenityScreen(
viewModel: AmenityFormViewModel = viewModel(),
amenityListViewModel: AmenityListViewModel = viewModel()
) {
androidx.compose.runtime.LaunchedEffect(Unit) {
viewModel.resetForm(preserveOptions = true)
}
AmenityFormScreen(
AmenityEditorScreen(
title = "Add Amenity",
setupKey = Unit,
onBack = onBack,
onSaveClick = { viewModel.submitCreate(onSave) },
setupForm = { viewModel.resetForm(preserveOptions = true) },
viewModel = viewModel,
amenityListViewModel = amenityListViewModel
)

View File

@@ -0,0 +1,27 @@
package com.android.trisolarispms.ui.roomtype
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.lifecycle.viewmodel.compose.viewModel
@Composable
internal fun AmenityEditorScreen(
title: String,
setupKey: Any?,
onBack: () -> Unit,
onSaveClick: () -> Unit,
setupForm: () -> Unit,
viewModel: AmenityFormViewModel = viewModel(),
amenityListViewModel: AmenityListViewModel = viewModel()
) {
LaunchedEffect(setupKey) {
setupForm()
}
AmenityFormScreen(
title = title,
onBack = onBack,
onSaveClick = onSaveClick,
viewModel = viewModel,
amenityListViewModel = amenityListViewModel
)
}

View File

@@ -3,12 +3,14 @@ package com.android.trisolarispms.ui.roomtype
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.data.api.core.ApiService
import com.android.trisolarispms.data.api.model.AmenityCreateRequest
import com.android.trisolarispms.data.api.model.AmenityUpdateRequest
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import retrofit2.Response
class AmenityFormViewModel : ViewModel() {
private val _state = MutableStateFlow(AmenityFormState())
@@ -88,62 +90,75 @@ class AmenityFormViewModel : ViewModel() {
}
fun submitCreate(onDone: () -> Unit) {
val name = state.value.name.trim()
if (name.isBlank()) {
_state.update { it.copy(error = "Name is required") }
return
}
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) }
try {
val iconKey = state.value.iconKey.trim().removeSuffix(".png").removeSuffix(".PNG")
val api = ApiClient.create()
val response = api.createAmenity(
AmenityCreateRequest(
name = name,
category = state.value.category.trim().ifBlank { null },
iconKey = iconKey.ifBlank { null }
)
)
if (response.isSuccessful) {
_state.update { it.copy(isLoading = false, success = true) }
onDone()
} else {
_state.update { it.copy(isLoading = false, error = "Create failed: ${response.code()}") }
}
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Create failed") }
}
val payload = readValidatedPayload() ?: return
submitMutation(
action = "Create",
onDone = onDone
) { api ->
api.createAmenity(payload.toCreateRequest())
}
}
fun submitUpdate(amenityId: String, onDone: () -> Unit) {
val name = state.value.name.trim()
val payload = readValidatedPayload() ?: return
submitMutation(
action = "Update",
onDone = onDone
) { api ->
api.updateAmenity(amenityId, payload.toUpdateRequest())
}
}
private data class AmenityPayload(
val name: String,
val category: String?,
val iconKey: String?
) {
fun toCreateRequest(): AmenityCreateRequest = AmenityCreateRequest(
name = name,
category = category,
iconKey = iconKey
)
fun toUpdateRequest(): AmenityUpdateRequest = AmenityUpdateRequest(
name = name,
category = category,
iconKey = iconKey
)
}
private fun readValidatedPayload(): AmenityPayload? {
val current = state.value
val name = current.name.trim()
if (name.isBlank()) {
_state.update { it.copy(error = "Name is required") }
return
return null
}
val iconKey = current.iconKey.trim().removeSuffix(".png").removeSuffix(".PNG")
return AmenityPayload(
name = name,
category = current.category.trim().ifBlank { null },
iconKey = iconKey.ifBlank { null }
)
}
private fun submitMutation(
action: String,
onDone: () -> Unit,
call: suspend (ApiService) -> Response<*>
) {
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) }
try {
val iconKey = state.value.iconKey.trim().removeSuffix(".png").removeSuffix(".PNG")
val api = ApiClient.create()
val response = api.updateAmenity(
amenityId,
AmenityUpdateRequest(
name = name,
category = state.value.category.trim().ifBlank { null },
iconKey = iconKey.ifBlank { null }
)
)
val response = call(ApiClient.create())
if (response.isSuccessful) {
_state.update { it.copy(isLoading = false, success = true) }
onDone()
} else {
_state.update { it.copy(isLoading = false, error = "Update failed: ${response.code()}") }
_state.update { it.copy(isLoading = false, error = "$action failed: ${response.code()}") }
}
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Update failed") }
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "$action failed") }
}
}
}

View File

@@ -1,21 +1,23 @@
package com.android.trisolarispms.ui.roomtype
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.core.viewmodel.launchRequest
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
class AmenityListViewModel : ViewModel() {
private val _state = MutableStateFlow(AmenityListState())
val state: StateFlow<AmenityListState> = _state
fun load() {
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) }
try {
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.listAmenities()
if (response.isSuccessful) {
@@ -29,17 +31,17 @@ class AmenityListViewModel : ViewModel() {
} else {
_state.update { it.copy(isLoading = false, error = "Load failed: ${response.code()}") }
}
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Load failed") }
}
}
}
fun deleteAmenity(amenityId: String) {
if (amenityId.isBlank()) return
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) }
try {
launchRequest(
state = _state,
setLoading = { it.copy(isLoading = true, error = null) },
setError = { current, message -> current.copy(isLoading = false, error = message) },
defaultError = "Delete failed"
) {
val api = ApiClient.create()
val response = api.deleteAmenity(amenityId)
if (response.isSuccessful) {
@@ -53,9 +55,6 @@ class AmenityListViewModel : ViewModel() {
} else {
_state.update { it.copy(isLoading = false, error = "Delete failed: ${response.code()}") }
}
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Delete failed") }
}
}
}
}

View File

@@ -1,7 +1,6 @@
package com.android.trisolarispms.ui.roomtype
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.lifecycle.viewmodel.compose.viewModel
import com.android.trisolarispms.data.api.model.AmenityDto
@@ -13,16 +12,15 @@ fun EditAmenityScreen(
viewModel: AmenityFormViewModel = viewModel(),
amenityListViewModel: AmenityListViewModel = viewModel()
) {
LaunchedEffect(amenity.id) {
viewModel.setAmenity(amenity)
}
AmenityFormScreen(
AmenityEditorScreen(
title = "Edit Amenity",
setupKey = amenity.id,
onBack = onBack,
onSaveClick = {
val id = amenity.id.orEmpty()
viewModel.submitUpdate(id, onSave)
},
setupForm = { viewModel.setAmenity(amenity) },
viewModel = viewModel,
amenityListViewModel = amenityListViewModel
)

View File

@@ -3,12 +3,14 @@ package com.android.trisolarispms.ui.roomtype
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.data.api.core.ApiService
import com.android.trisolarispms.data.api.model.RoomTypeCreateRequest
import com.android.trisolarispms.data.api.model.RoomTypeUpdateRequest
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import retrofit2.Response
class RoomTypeFormViewModel : ViewModel() {
private val _state = MutableStateFlow(RoomTypeFormState())
@@ -48,70 +50,22 @@ class RoomTypeFormViewModel : ViewModel() {
fun onAliasesChange(value: String) = _state.update { it.copy(otaAliases = value, error = null) }
fun submit(propertyId: String, onDone: () -> Unit) {
val code = state.value.code.trim()
val name = state.value.name.trim()
if (code.isBlank() || name.isBlank()) {
_state.update { it.copy(error = "Code and name are required") }
return
}
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) }
try {
val api = ApiClient.create()
val body = RoomTypeCreateRequest(
code = code,
name = name,
baseOccupancy = state.value.baseOccupancy.toIntOrNull(),
maxOccupancy = state.value.maxOccupancy.toIntOrNull(),
sqFeet = state.value.sqFeet.toIntOrNull(),
bathroomSqFeet = state.value.bathroomSqFeet.toIntOrNull(),
amenityIds = state.value.amenityIds.toList().ifEmpty { null },
otaAliases = state.value.otaAliases.split(',').map { it.trim() }.filter { it.isNotBlank() }.ifEmpty { null }
)
val response = api.createRoomType(propertyId, body)
if (response.isSuccessful) {
_state.update { it.copy(isLoading = false, success = true) }
onDone()
} else {
_state.update { it.copy(isLoading = false, error = "Create failed: ${response.code()}") }
}
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Create failed") }
}
val payload = readValidatedPayload() ?: return
submitMutation(
action = "Create",
onDone = onDone
) { api ->
api.createRoomType(propertyId, payload.toCreateRequest())
}
}
fun submitUpdate(propertyId: String, roomTypeId: String, onDone: () -> Unit) {
val code = state.value.code.trim()
val name = state.value.name.trim()
if (code.isBlank() || name.isBlank()) {
_state.update { it.copy(error = "Code and name are required") }
return
}
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) }
try {
val api = ApiClient.create()
val body = RoomTypeUpdateRequest(
code = code,
name = name,
baseOccupancy = state.value.baseOccupancy.toIntOrNull(),
maxOccupancy = state.value.maxOccupancy.toIntOrNull(),
sqFeet = state.value.sqFeet.toIntOrNull(),
bathroomSqFeet = state.value.bathroomSqFeet.toIntOrNull(),
amenityIds = state.value.amenityIds.toList().ifEmpty { null },
otaAliases = state.value.otaAliases.split(',').map { it.trim() }.filter { it.isNotBlank() }.ifEmpty { null }
)
val response = api.updateRoomType(propertyId, roomTypeId, body)
if (response.isSuccessful) {
_state.update { it.copy(isLoading = false, success = true) }
onDone()
} else {
_state.update { it.copy(isLoading = false, error = "Update failed: ${response.code()}") }
}
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Update failed") }
}
val payload = readValidatedPayload() ?: return
submitMutation(
action = "Update",
onDone = onDone
) { api ->
api.updateRoomType(propertyId, roomTypeId, payload.toUpdateRequest())
}
}
@@ -120,19 +74,88 @@ class RoomTypeFormViewModel : ViewModel() {
_state.update { it.copy(error = "Room type ID is missing") }
return
}
submitMutation(
action = "Delete",
onDone = onDone
) { api ->
api.deleteRoomType(propertyId, roomTypeId)
}
}
private data class RoomTypePayload(
val code: String,
val name: String,
val baseOccupancy: Int?,
val maxOccupancy: Int?,
val sqFeet: Int?,
val bathroomSqFeet: Int?,
val amenityIds: List<String>?,
val otaAliases: List<String>?
) {
fun toCreateRequest(): RoomTypeCreateRequest = RoomTypeCreateRequest(
code = code,
name = name,
baseOccupancy = baseOccupancy,
maxOccupancy = maxOccupancy,
sqFeet = sqFeet,
bathroomSqFeet = bathroomSqFeet,
amenityIds = amenityIds,
otaAliases = otaAliases
)
fun toUpdateRequest(): RoomTypeUpdateRequest = RoomTypeUpdateRequest(
code = code,
name = name,
baseOccupancy = baseOccupancy,
maxOccupancy = maxOccupancy,
sqFeet = sqFeet,
bathroomSqFeet = bathroomSqFeet,
amenityIds = amenityIds,
otaAliases = otaAliases
)
}
private fun readValidatedPayload(): RoomTypePayload? {
val current = state.value
val code = current.code.trim()
val name = current.name.trim()
if (code.isBlank() || name.isBlank()) {
_state.update { it.copy(error = "Code and name are required") }
return null
}
return RoomTypePayload(
code = code,
name = name,
baseOccupancy = current.baseOccupancy.toIntOrNull(),
maxOccupancy = current.maxOccupancy.toIntOrNull(),
sqFeet = current.sqFeet.toIntOrNull(),
bathroomSqFeet = current.bathroomSqFeet.toIntOrNull(),
amenityIds = current.amenityIds.toList().ifEmpty { null },
otaAliases = current.otaAliases
.split(',')
.map { it.trim() }
.filter { it.isNotBlank() }
.ifEmpty { null }
)
}
private fun submitMutation(
action: String,
onDone: () -> Unit,
call: suspend (ApiService) -> Response<*>
) {
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) }
try {
val api = ApiClient.create()
val response = api.deleteRoomType(propertyId, roomTypeId)
val response = call(ApiClient.create())
if (response.isSuccessful) {
_state.update { it.copy(isLoading = false, success = true) }
onDone()
} else {
_state.update { it.copy(isLoading = false, error = "Delete failed: ${response.code()}") }
_state.update { it.copy(isLoading = false, error = "$action failed: ${response.code()}") }
}
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Delete failed") }
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "$action failed") }
}
}
}

View File

@@ -3,6 +3,9 @@ package com.android.trisolarispms.ui.roomtype
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.data.api.core.ApiService
import com.android.trisolarispms.data.api.model.RoomTypeDto
import com.android.trisolarispms.core.viewmodel.launchRequest
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
@@ -14,9 +17,12 @@ class RoomTypeListViewModel : ViewModel() {
fun load(propertyId: String) {
if (propertyId.isBlank()) return
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) }
try {
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.listRoomTypes(propertyId)
if (response.isSuccessful) {
@@ -33,57 +39,59 @@ class RoomTypeListViewModel : ViewModel() {
} else {
_state.update { it.copy(isLoading = false, error = "Load failed: ${response.code()}") }
}
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Load failed") }
}
}
}
private fun loadRoomTypeImages(propertyId: String, items: List<com.android.trisolarispms.data.api.model.RoomTypeDto>) {
private fun loadRoomTypeImages(propertyId: String, items: List<RoomTypeDto>) {
viewModelScope.launch {
val api = ApiClient.create()
val updates = mutableMapOf<String, com.android.trisolarispms.data.api.model.ImageDto?>()
for (item in items) {
val code = item.code?.trim().orEmpty()
if (code.isBlank()) continue
try {
val updates = buildByTypeCodeMap(
items = items,
fetch = { api, code ->
val response = api.listRoomTypeImages(propertyId, code)
if (response.isSuccessful) {
updates[code] = response.body().orEmpty().firstOrNull()
}
} catch (_: Exception) {
// Ignore per-item failures to avoid blocking the list.
}
if (response.isSuccessful) response.body().orEmpty().firstOrNull() else null
}
)
if (updates.isNotEmpty()) {
_state.update { it.copy(imageByTypeCode = it.imageByTypeCode + updates) }
}
}
}
private fun loadRoomTypeAvailableCounts(propertyId: String, items: List<com.android.trisolarispms.data.api.model.RoomTypeDto>) {
private fun loadRoomTypeAvailableCounts(propertyId: String, items: List<RoomTypeDto>) {
viewModelScope.launch {
val api = ApiClient.create()
val updates = mutableMapOf<String, Int>()
for (item in items) {
val code = item.code?.trim().orEmpty()
if (code.isBlank()) continue
try {
val updates = buildByTypeCodeMap(
items = items,
fetch = { api, code ->
val response = api.listRoomsByType(
propertyId = propertyId,
roomTypeCode = code,
availableOnly = true
)
if (response.isSuccessful) {
updates[code] = response.body().orEmpty().size
}
} catch (_: Exception) {
// Ignore per-item failures.
response.body().orEmpty().size
} else {
null
}
}
)
if (updates.isNotEmpty()) {
_state.update { it.copy(availableCountByTypeCode = it.availableCountByTypeCode + updates) }
}
}
}
private suspend fun <T> buildByTypeCodeMap(
items: List<RoomTypeDto>,
fetch: suspend (ApiService, String) -> T?
): Map<String, T> {
val api = ApiClient.create()
val updates = mutableMapOf<String, T>()
for (item in items) {
val code = item.code?.trim().orEmpty()
if (code.isBlank()) continue
val value = runCatching { fetch(api, code) }.getOrNull() ?: continue
updates[code] = value
}
return updates
}
}

View File

@@ -3,9 +3,10 @@ package com.android.trisolarispms.ui.users
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.data.api.core.ApiService
import com.android.trisolarispms.data.api.model.PropertyAccessCodeCreateRequest
import com.android.trisolarispms.data.api.model.PropertyAccessCodeResponse
import com.android.trisolarispms.data.api.model.PropertyUserResponse
import com.android.trisolarispms.data.api.model.PropertyUserDetailsResponse
import com.android.trisolarispms.data.api.model.PropertyAccessCodeJoinRequest
import com.android.trisolarispms.data.api.model.UserRolesUpdateRequest
import com.android.trisolarispms.data.api.model.PropertyUserDisabledRequest
@@ -28,33 +29,7 @@ class PropertyUsersViewModel : ViewModel() {
val state: StateFlow<PropertyUsersState> = _state
fun loadAll(propertyId: String) {
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null, message = null) }
try {
val response = ApiClient.create().searchPropertyUsers(
propertyId = propertyId,
phone = null
)
val body = response.body()
if (response.isSuccessful && body != null) {
val mapped = body.map {
PropertyUserUi(
userId = it.userId,
roles = it.roles,
name = it.name,
phoneE164 = it.phoneE164,
disabled = it.disabled,
superAdmin = it.superAdmin
)
}
_state.update { it.copy(isLoading = false, users = mapped, error = null) }
} else {
_state.update { it.copy(isLoading = false, error = "Load failed: ${response.code()}") }
}
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Load failed") }
}
}
fetchUsers(propertyId = propertyId, phone = null, action = "Load")
}
fun searchUsers(propertyId: String, phoneInput: String) {
@@ -63,33 +38,7 @@ class PropertyUsersViewModel : ViewModel() {
_state.update { it.copy(users = emptyList(), error = null, isLoading = false, message = null) }
return
}
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null, message = null) }
try {
val response = ApiClient.create().searchPropertyUsers(
propertyId = propertyId,
phone = digits
)
val body = response.body()
if (response.isSuccessful && body != null) {
val mapped = body.map {
PropertyUserUi(
userId = it.userId,
roles = it.roles,
name = it.name,
phoneE164 = it.phoneE164,
disabled = it.disabled,
superAdmin = it.superAdmin
)
}
_state.update { it.copy(isLoading = false, users = mapped, error = null) }
} else {
_state.update { it.copy(isLoading = false, error = "Search failed: ${response.code()}") }
}
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Search failed") }
}
}
fetchUsers(propertyId = propertyId, phone = digits, action = "Search")
}
fun createAccessCode(propertyId: String, roles: List<String>) {
@@ -97,10 +46,8 @@ class PropertyUsersViewModel : ViewModel() {
_state.update { it.copy(error = "Select at least one role", message = null) }
return
}
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null, message = null) }
try {
val response = ApiClient.create().createAccessCode(
runAction(defaultError = "Create failed") { api ->
val response = api.createAccessCode(
propertyId = propertyId,
body = PropertyAccessCodeCreateRequest(roles = roles)
)
@@ -116,9 +63,6 @@ class PropertyUsersViewModel : ViewModel() {
} else {
_state.update { it.copy(isLoading = false, error = "Create failed: ${response.code()}") }
}
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Create failed") }
}
}
}
@@ -128,10 +72,8 @@ class PropertyUsersViewModel : ViewModel() {
_state.update { it.copy(error = "Code must be 6 digits", message = null) }
return
}
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null, message = null) }
try {
val response = ApiClient.create().joinAccessCode(
runAction(defaultError = "Join failed") { api ->
val response = api.joinAccessCode(
PropertyAccessCodeJoinRequest(propertyId = propertyId, code = digits)
)
val body = response.body()
@@ -146,9 +88,6 @@ class PropertyUsersViewModel : ViewModel() {
} else {
_state.update { it.copy(isLoading = false, error = "Join failed: ${response.code()}") }
}
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Join failed") }
}
}
}
@@ -157,10 +96,8 @@ class PropertyUsersViewModel : ViewModel() {
_state.update { it.copy(error = "Select at least one role", message = null) }
return
}
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null, message = null) }
try {
val response = ApiClient.create().updateUserRoles(
runAction(defaultError = "Update failed") { api ->
val response = api.updateUserRoles(
propertyId = propertyId,
userId = userId,
body = UserRolesUpdateRequest(roles = roles)
@@ -169,9 +106,7 @@ class PropertyUsersViewModel : ViewModel() {
if (response.isSuccessful && body != null) {
_state.update { current ->
val updated = current.users.map { user ->
if (user.userId == userId) {
user.copy(roles = body.roles)
} else user
if (user.userId == userId) user.copy(roles = body.roles) else user
}
current.copy(
isLoading = false,
@@ -182,17 +117,12 @@ class PropertyUsersViewModel : ViewModel() {
} else {
_state.update { it.copy(isLoading = false, error = "Update failed: ${response.code()}") }
}
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Update failed") }
}
}
}
fun updateDisabled(propertyId: String, userId: String, disabled: Boolean) {
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null, message = null) }
try {
val response = ApiClient.create().updateUserDisabled(
runAction(defaultError = "Update failed") { api ->
val response = api.updateUserDisabled(
propertyId = propertyId,
userId = userId,
body = PropertyUserDisabledRequest(disabled = disabled)
@@ -200,9 +130,7 @@ class PropertyUsersViewModel : ViewModel() {
if (response.isSuccessful) {
_state.update { current ->
val updated = current.users.map { user ->
if (user.userId == userId) {
user.copy(disabled = disabled)
} else user
if (user.userId == userId) user.copy(disabled = disabled) else user
}
current.copy(
isLoading = false,
@@ -213,9 +141,6 @@ class PropertyUsersViewModel : ViewModel() {
} else {
_state.update { it.copy(isLoading = false, error = "Update failed: ${response.code()}") }
}
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Update failed") }
}
}
}
@@ -232,4 +157,41 @@ class PropertyUsersViewModel : ViewModel() {
}
}
}
private fun fetchUsers(propertyId: String, phone: String?, action: String) {
runAction(defaultError = "$action failed") { api ->
val response = api.searchPropertyUsers(propertyId = propertyId, phone = phone)
val body = response.body()
if (response.isSuccessful && body != null) {
_state.update { it.copy(isLoading = false, users = mapPropertyUsers(body), error = null) }
} else {
_state.update { it.copy(isLoading = false, error = "$action failed: ${response.code()}") }
}
}
}
private fun mapPropertyUsers(body: List<PropertyUserDetailsResponse>): List<PropertyUserUi> = body.map {
PropertyUserUi(
userId = it.userId,
roles = it.roles,
name = it.name,
phoneE164 = it.phoneE164,
disabled = it.disabled,
superAdmin = it.superAdmin
)
}
private fun runAction(
defaultError: String,
block: suspend (ApiService) -> Unit
) {
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null, message = null) }
try {
block(ApiClient.create())
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: defaultError) }
}
}
}
}

View File

@@ -3,6 +3,8 @@ package com.android.trisolarispms.ui.users
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.data.api.core.ApiService
import com.android.trisolarispms.core.viewmodel.launchRequest
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
@@ -25,37 +27,11 @@ class UserDirectoryViewModel : ViewModel() {
val state: StateFlow<UserDirectoryState> = _state
fun loadAll(mode: UserDirectoryMode) {
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) }
try {
val api = ApiClient.create()
when (mode) {
UserDirectoryMode.SuperAdmin -> {
val response = api.listUsers(null)
val users = response.body()?.toSuperAdminUsers()
if (response.isSuccessful && users != null) {
_state.update { it.copy(isLoading = false, users = users, error = null) }
} else {
_state.update { it.copy(isLoading = false, error = "Load failed: ${response.code()}") }
}
}
is UserDirectoryMode.Property -> {
val response = api.searchPropertyUsers(
propertyId = mode.propertyId,
phone = null
runDirectoryQuery(
mode = mode,
phone = null,
action = "Load"
)
val users = response.body()?.toPropertyUsers()
if (response.isSuccessful && users != null) {
_state.update { it.copy(isLoading = false, users = users, error = null) }
} else {
_state.update { it.copy(isLoading = false, error = "Load failed: ${response.code()}") }
}
}
}
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Load failed") }
}
}
}
fun search(mode: UserDirectoryMode, phoneInput: String) {
@@ -64,36 +40,62 @@ class UserDirectoryViewModel : ViewModel() {
_state.update { it.copy(users = emptyList(), error = null, isLoading = false) }
return
}
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) }
try {
val api = ApiClient.create()
when (mode) {
UserDirectoryMode.SuperAdmin -> {
val response = api.listUsers(digits)
val users = response.body()?.toSuperAdminUsers()
if (response.isSuccessful && users != null) {
_state.update { it.copy(isLoading = false, users = users, error = null) }
} else {
_state.update { it.copy(isLoading = false, error = "Search failed: ${response.code()}") }
runDirectoryQuery(
mode = mode,
phone = digits,
action = "Search"
)
}
private data class UserQueryResult(
val users: List<PropertyUserUi>?,
val code: Int,
val isSuccessful: Boolean
)
private fun runDirectoryQuery(
mode: UserDirectoryMode,
phone: String?,
action: String
) {
launchRequest(
state = _state,
setLoading = { it.copy(isLoading = true, error = null) },
setError = { current, message -> current.copy(isLoading = false, error = message) },
defaultError = "$action failed"
) {
val result = queryUsers(ApiClient.create(), mode, phone)
if (result.isSuccessful && result.users != null) {
_state.update { it.copy(isLoading = false, users = result.users, error = null) }
} else {
_state.update { it.copy(isLoading = false, error = "$action failed: ${result.code}") }
}
}
}
private suspend fun queryUsers(
api: ApiService,
mode: UserDirectoryMode,
phone: String?
): UserQueryResult = when (mode) {
UserDirectoryMode.SuperAdmin -> {
val response = api.listUsers(phone)
UserQueryResult(
users = response.body()?.toSuperAdminUsers(),
code = response.code(),
isSuccessful = response.isSuccessful
)
}
is UserDirectoryMode.Property -> {
val response = api.searchPropertyUsers(
propertyId = mode.propertyId,
phone = digits
phone = phone
)
UserQueryResult(
users = response.body()?.toPropertyUsers(),
code = response.code(),
isSuccessful = response.isSuccessful
)
val users = response.body()?.toPropertyUsers()
if (response.isSuccessful && users != null) {
_state.update { it.copy(isLoading = false, users = users, error = null) }
} else {
_state.update { it.copy(isLoading = false, error = "Search failed: ${response.code()}") }
}
}
}
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Search failed") }
}
}
}