ai removed boilerplate and orgnized code even more
This commit is contained in:
@@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user