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

View File

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

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.RazorpayQrScreen
import com.android.trisolarispms.ui.razorpay.RazorpaySettingsScreen import com.android.trisolarispms.ui.razorpay.RazorpaySettingsScreen
import com.android.trisolarispms.ui.roomstay.ActiveRoomStaysScreen 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.ManageRoomStayRatesScreen
import com.android.trisolarispms.ui.roomstay.ManageRoomStaySelectScreen import com.android.trisolarispms.ui.roomstay.ManageRoomStaySelectScreen
@@ -18,23 +19,56 @@ internal fun renderStayFlowRoutes(
authViewModel: AuthViewModel, authViewModel: AuthViewModel,
authz: AuthzPolicy authz: AuthzPolicy
): Boolean { ): 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) { when (val currentRoute = refs.currentRoute) {
is AppRoute.ActiveRoomStays -> ActiveRoomStaysScreen( is AppRoute.ActiveRoomStays -> ActiveRoomStaysScreen(
propertyId = currentRoute.propertyId, propertyId = currentRoute.propertyId,
propertyName = currentRoute.propertyName, propertyName = currentRoute.propertyName,
onBack = { onBack = {
val blockBack = authz.shouldBlockBackToHome( val blockBack = shouldBlockHomeBack(authz, state, currentRoute.propertyId)
propertyId = currentRoute.propertyId,
propertyCount = state.propertyRoles.size
)
if (!blockBack) { if (!blockBack) {
refs.route.value = AppRoute.Home refs.route.value = AppRoute.Home
} }
}, },
showBack = !authz.shouldBlockBackToHome( showBack = !shouldBlockHomeBack(authz, state, currentRoute.propertyId),
propertyId = currentRoute.propertyId,
propertyCount = state.propertyRoles.size
),
onViewRooms = { refs.route.value = AppRoute.Rooms(currentRoute.propertyId) }, onViewRooms = { refs.route.value = AppRoute.Rooms(currentRoute.propertyId) },
onCreateBooking = { refs.route.value = AppRoute.CreateBooking(currentRoute.propertyId) }, onCreateBooking = { refs.route.value = AppRoute.CreateBooking(currentRoute.propertyId) },
canCreateBooking = authz.canCreateBookingFor(currentRoute.propertyId), canCreateBooking = authz.canCreateBookingFor(currentRoute.propertyId),
@@ -91,62 +125,54 @@ internal fun renderStayFlowRoutes(
} }
) )
is AppRoute.ManageRoomStaySelect -> ManageRoomStaySelectScreen( is AppRoute.ManageRoomStaySelect -> renderManageRoomStaySelectRoute(
propertyId = currentRoute.propertyId, propertyId = currentRoute.propertyId,
bookingFromAt = currentRoute.fromAt, fromAt = currentRoute.fromAt,
bookingToAt = currentRoute.toAt, toAt = currentRoute.toAt
onBack = { refs.openActiveRoomStays(currentRoute.propertyId) }, ) {
onNext = { rooms -> refs.route.value = AppRoute.ManageRoomStayRates(
refs.selectedManageRooms.value = rooms propertyId = currentRoute.propertyId,
refs.route.value = AppRoute.ManageRoomStayRates( bookingId = currentRoute.bookingId,
propertyId = currentRoute.propertyId, fromAt = currentRoute.fromAt,
bookingId = currentRoute.bookingId, toAt = currentRoute.toAt
fromAt = currentRoute.fromAt, )
toAt = currentRoute.toAt }
)
}
)
is AppRoute.ManageRoomStaySelectFromBooking -> ManageRoomStaySelectScreen( is AppRoute.ManageRoomStaySelectFromBooking -> renderManageRoomStaySelectRoute(
propertyId = currentRoute.propertyId, propertyId = currentRoute.propertyId,
bookingFromAt = currentRoute.fromAt, fromAt = currentRoute.fromAt,
bookingToAt = currentRoute.toAt, toAt = currentRoute.toAt
onBack = { refs.openActiveRoomStays(currentRoute.propertyId) }, ) {
onNext = { rooms -> refs.route.value = AppRoute.ManageRoomStayRatesFromBooking(
refs.selectedManageRooms.value = rooms propertyId = currentRoute.propertyId,
refs.route.value = AppRoute.ManageRoomStayRatesFromBooking( bookingId = currentRoute.bookingId,
propertyId = currentRoute.propertyId, guestId = currentRoute.guestId,
bookingId = currentRoute.bookingId, fromAt = currentRoute.fromAt,
guestId = currentRoute.guestId, toAt = currentRoute.toAt
fromAt = currentRoute.fromAt, )
toAt = currentRoute.toAt }
)
}
)
is AppRoute.ManageRoomStayRates -> ManageRoomStayRatesScreen( is AppRoute.ManageRoomStayRates -> renderManageRoomStayRatesRoute(
propertyId = currentRoute.propertyId, propertyId = currentRoute.propertyId,
bookingId = currentRoute.bookingId, bookingId = currentRoute.bookingId,
checkInAt = currentRoute.fromAt, fromAt = currentRoute.fromAt,
checkOutAt = currentRoute.toAt, toAt = currentRoute.toAt,
selectedRooms = refs.selectedManageRooms.value,
onBack = { onBack = {
refs.route.value = AppRoute.ManageRoomStaySelect( refs.route.value = AppRoute.ManageRoomStaySelect(
currentRoute.propertyId, propertyId = currentRoute.propertyId,
currentRoute.bookingId, bookingId = currentRoute.bookingId,
currentRoute.fromAt, fromAt = currentRoute.fromAt,
currentRoute.toAt toAt = currentRoute.toAt
) )
}, },
onDone = { refs.openActiveRoomStays(currentRoute.propertyId) } onDone = { refs.openActiveRoomStays(currentRoute.propertyId) }
) )
is AppRoute.ManageRoomStayRatesFromBooking -> ManageRoomStayRatesScreen( is AppRoute.ManageRoomStayRatesFromBooking -> renderManageRoomStayRatesRoute(
propertyId = currentRoute.propertyId, propertyId = currentRoute.propertyId,
bookingId = currentRoute.bookingId, bookingId = currentRoute.bookingId,
checkInAt = currentRoute.fromAt, fromAt = currentRoute.fromAt,
checkOutAt = currentRoute.toAt, toAt = currentRoute.toAt,
selectedRooms = refs.selectedManageRooms.value,
onBack = { onBack = {
refs.route.value = AppRoute.ManageRoomStaySelectFromBooking( refs.route.value = AppRoute.ManageRoomStaySelectFromBooking(
currentRoute.propertyId, currentRoute.propertyId,
@@ -169,3 +195,9 @@ internal fun renderStayFlowRoutes(
} }
return true return true
} }
private fun shouldBlockHomeBack(authz: AuthzPolicy, state: AuthUiState, propertyId: String): Boolean =
authz.shouldBlockBackToHome(
propertyId = propertyId,
propertyCount = state.propertyRoles.size
)

View File

@@ -3,49 +3,33 @@ package com.android.trisolarispms.ui.payment
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.core.ApiClient 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 com.android.trisolarispms.data.api.model.RazorpayRefundRequest
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import retrofit2.Response
class BookingPaymentsViewModel : ViewModel() { class BookingPaymentsViewModel : ViewModel() {
private val _state = MutableStateFlow(BookingPaymentsState()) private val _state = MutableStateFlow(BookingPaymentsState())
val state: StateFlow<BookingPaymentsState> = _state val state: StateFlow<BookingPaymentsState> = _state
fun load(propertyId: String, bookingId: String) { fun load(propertyId: String, bookingId: String) {
viewModelScope.launch { runPaymentAction(defaultError = "Load failed") { api ->
_state.update { it.copy(isLoading = true, error = null, message = null) } val response = api.listPayments(propertyId, bookingId)
try { val body = response.body()
val api = ApiClient.create() if (response.isSuccessful && body != null) {
val response = api.listPayments(propertyId, bookingId)
val body = response.body()
if (response.isSuccessful && body != null) {
_state.update {
it.copy(
isLoading = false,
payments = body,
error = null,
message = null
)
}
} else {
_state.update {
it.copy(
isLoading = false,
error = "Load failed: ${response.code()}",
message = null
)
}
}
} catch (e: Exception) {
_state.update { _state.update {
it.copy( it.copy(
isLoading = false, isLoading = false,
error = e.localizedMessage ?: "Load failed", payments = body,
error = null,
message = null message = null
) )
} }
} else {
setActionFailure("Load", response)
} }
} }
} }
@@ -55,82 +39,46 @@ class BookingPaymentsViewModel : ViewModel() {
_state.update { it.copy(error = "Amount must be greater than 0", message = null) } _state.update { it.copy(error = "Amount must be greater than 0", message = null) }
return return
} }
viewModelScope.launch { runPaymentAction(defaultError = "Create failed") { api ->
_state.update { it.copy(isLoading = true, error = null, message = null) } val response = api.createPayment(
try { propertyId = propertyId,
val api = ApiClient.create() bookingId = bookingId,
val response = api.createPayment( body = com.android.trisolarispms.data.api.model.PaymentCreateRequest(amount = amount)
propertyId = propertyId, )
bookingId = bookingId, val body = response.body()
body = com.android.trisolarispms.data.api.model.PaymentCreateRequest(amount = amount) if (response.isSuccessful && body != null) {
) _state.update { current ->
val body = response.body() current.copy(
if (response.isSuccessful && body != null) {
_state.update { current ->
current.copy(
isLoading = false,
payments = listOf(body) + current.payments,
error = null,
message = "Cash payment added"
)
}
} else {
_state.update {
it.copy(
isLoading = false,
error = "Create failed: ${response.code()}",
message = null
)
}
}
} catch (e: Exception) {
_state.update {
it.copy(
isLoading = false, isLoading = false,
error = e.localizedMessage ?: "Create failed", payments = listOf(body) + current.payments,
message = null error = null,
message = "Cash payment added"
) )
} }
} else {
setActionFailure("Create", response)
} }
} }
} }
fun deleteCashPayment(propertyId: String, bookingId: String, paymentId: String) { fun deleteCashPayment(propertyId: String, bookingId: String, paymentId: String) {
viewModelScope.launch { runPaymentAction(defaultError = "Delete failed") { api ->
_state.update { it.copy(isLoading = true, error = null, message = null) } val response = api.deletePayment(
try { propertyId = propertyId,
val api = ApiClient.create() bookingId = bookingId,
val response = api.deletePayment( paymentId = paymentId
propertyId = propertyId, )
bookingId = bookingId, if (response.isSuccessful) {
paymentId = paymentId _state.update { current ->
) current.copy(
if (response.isSuccessful) {
_state.update { current ->
current.copy(
isLoading = false,
payments = current.payments.filterNot { it.id == paymentId },
error = null,
message = "Cash payment deleted"
)
}
} else {
_state.update {
it.copy(
isLoading = false,
error = "Delete failed: ${response.code()}",
message = null
)
}
}
} catch (e: Exception) {
_state.update {
it.copy(
isLoading = false, isLoading = false,
error = e.localizedMessage ?: "Delete failed", payments = current.payments.filterNot { it.id == paymentId },
message = null error = null,
message = "Cash payment deleted"
) )
} }
} else {
setActionFailure("Delete", response)
} }
} }
} }
@@ -151,48 +99,60 @@ class BookingPaymentsViewModel : ViewModel() {
_state.update { it.copy(error = "Missing payment ID", message = null) } _state.update { it.copy(error = "Missing payment ID", message = null) }
return return
} }
runPaymentAction(defaultError = "Refund failed") { api ->
val response = api.refundRazorpayPayment(
propertyId = propertyId,
bookingId = bookingId,
body = RazorpayRefundRequest(
paymentId = paymentId,
razorpayPaymentId = razorpayPaymentId,
amount = amount,
notes = notes?.takeIf { it.isNotBlank() }
)
)
val body = response.body()
if (response.isSuccessful && body != null) {
_state.update {
it.copy(
isLoading = false,
error = null,
message = "Refund processed"
)
}
load(propertyId, bookingId)
} else {
setActionFailure("Refund", response)
}
}
}
private fun runPaymentAction(
defaultError: String,
block: suspend (ApiService) -> Unit
) {
viewModelScope.launch { viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null, message = null) } _state.update { it.copy(isLoading = true, error = null, message = null) }
try { try {
val api = ApiClient.create() block(ApiClient.create())
val response = api.refundRazorpayPayment(
propertyId = propertyId,
bookingId = bookingId,
body = RazorpayRefundRequest(
paymentId = paymentId,
razorpayPaymentId = razorpayPaymentId,
amount = amount,
notes = notes?.takeIf { it.isNotBlank() }
)
)
val body = response.body()
if (response.isSuccessful && body != null) {
_state.update {
it.copy(
isLoading = false,
error = null,
message = "Refund processed"
)
}
load(propertyId, bookingId)
} else {
_state.update {
it.copy(
isLoading = false,
error = "Refund failed: ${response.code()}",
message = null
)
}
}
} catch (e: Exception) { } catch (e: Exception) {
_state.update { _state.update {
it.copy( it.copy(
isLoading = false, isLoading = false,
error = e.localizedMessage ?: "Refund failed", error = e.localizedMessage ?: defaultError,
message = null 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 package com.android.trisolarispms.ui.property
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.core.ApiClient import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.data.api.model.PropertyCreateRequest import com.android.trisolarispms.data.api.model.PropertyCreateRequest
import com.android.trisolarispms.core.viewmodel.launchRequest
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
class AddPropertyViewModel : ViewModel() { class AddPropertyViewModel : ViewModel() {
private val _state = MutableStateFlow(AddPropertyState()) private val _state = MutableStateFlow(AddPropertyState())
@@ -56,31 +55,31 @@ class AddPropertyViewModel : ViewModel() {
return return
} }
viewModelScope.launch { launchRequest(
_state.update { it.copy(isLoading = true, error = null) } state = _state,
try { setLoading = { it.copy(isLoading = true, error = null) },
val api = ApiClient.create() setError = { current, message -> current.copy(isLoading = false, error = message) },
val body = PropertyCreateRequest( defaultError = "Create failed"
code = null, ) {
name = name, val api = ApiClient.create()
addressText = state.value.addressText.takeIf { it.isNotBlank() }, val body = PropertyCreateRequest(
timezone = state.value.timezone.takeIf { it.isNotBlank() }, code = null,
currency = state.value.currency.takeIf { it.isNotBlank() } name = name,
) addressText = state.value.addressText.takeIf { it.isNotBlank() },
val response = api.createProperty(body) timezone = state.value.timezone.takeIf { it.isNotBlank() },
if (response.isSuccessful) { currency = state.value.currency.takeIf { it.isNotBlank() }
_state.update { )
it.copy( val response = api.createProperty(body)
isLoading = false, if (response.isSuccessful) {
createdPropertyId = response.body()?.id, _state.update {
error = null it.copy(
) isLoading = false,
} createdPropertyId = response.body()?.id,
} else { error = null
_state.update { it.copy(isLoading = false, error = "Create failed: ${response.code()}") } )
} }
} catch (e: Exception) { } else {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Create failed") } _state.update { it.copy(isLoading = false, error = "Create failed: ${response.code()}") }
} }
} }
} }

View File

@@ -1,36 +1,35 @@
package com.android.trisolarispms.ui.property package com.android.trisolarispms.ui.property
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.core.ApiClient import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.core.viewmodel.launchRequest
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
class PropertyListViewModel : ViewModel() { class PropertyListViewModel : ViewModel() {
private val _state = MutableStateFlow(PropertyListState()) private val _state = MutableStateFlow(PropertyListState())
val state: StateFlow<PropertyListState> = _state val state: StateFlow<PropertyListState> = _state
fun refresh() { fun refresh() {
viewModelScope.launch { launchRequest(
_state.update { it.copy(isLoading = true, error = null) } state = _state,
try { setLoading = { it.copy(isLoading = true, error = null) },
val api = ApiClient.create() setError = { current, message -> current.copy(isLoading = false, error = message) },
val response = api.listProperties() defaultError = "Load failed"
if (response.isSuccessful) { ) {
_state.update { val api = ApiClient.create()
it.copy( val response = api.listProperties()
isLoading = false, if (response.isSuccessful) {
properties = response.body().orEmpty(), _state.update {
error = null it.copy(
) isLoading = false,
} properties = response.body().orEmpty(),
} else { error = null
_state.update { it.copy(isLoading = false, error = "Load failed: ${response.code()}") } )
} }
} catch (e: Exception) { } else {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Load failed") } _state.update { it.copy(isLoading = false, error = "Load failed: ${response.code()}") }
} }
} }
} }

View File

@@ -3,12 +3,14 @@ package com.android.trisolarispms.ui.room
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.core.ApiClient 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.RoomCreateRequest
import com.android.trisolarispms.data.api.model.RoomUpdateRequest import com.android.trisolarispms.data.api.model.RoomUpdateRequest
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import retrofit2.Response
class RoomFormViewModel : ViewModel() { class RoomFormViewModel : ViewModel() {
private val _state = MutableStateFlow(RoomFormState()) 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 onNotesChange(value: String) = _state.update { it.copy(notes = value, error = null) }
fun submitCreate(propertyId: String, onDone: () -> Unit) { fun submitCreate(propertyId: String, onDone: () -> Unit) {
val roomNumberText = state.value.roomNumber.trim() val input = readValidatedInput() ?: return
val roomTypeCode = state.value.roomTypeCode.trim() submitMutation(
val roomNumber = roomNumberText.toIntOrNull() action = "Create",
if (roomNumber == null || roomTypeCode.isBlank()) { onDone = onDone
_state.update { it.copy(error = "Room number must be a number and room type is required") } ) { api ->
return api.createRoom(propertyId, input.toCreateRequest())
}
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") }
}
} }
} }
fun submitUpdate(propertyId: String, roomId: String, onDone: () -> Unit) { fun submitUpdate(propertyId: String, roomId: String, onDone: () -> Unit) {
val roomNumberText = state.value.roomNumber.trim() val input = readValidatedInput() ?: return
val roomTypeCode = state.value.roomTypeCode.trim() submitMutation(
val roomNumber = roomNumberText.toIntOrNull() action = "Update",
if (roomNumber == null || roomTypeCode.isBlank()) { onDone = onDone
_state.update { it.copy(error = "Room number must be a number and room type is required") } ) { api ->
return api.updateRoom(propertyId, roomId, input.toUpdateRequest())
}
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") }
}
} }
} }
@@ -130,19 +84,80 @@ class RoomFormViewModel : ViewModel() {
_state.update { it.copy(error = "Room ID is missing") } _state.update { it.copy(error = "Room ID is missing") }
return 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 { viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) } _state.update { it.copy(isLoading = true, error = null) }
try { try {
val api = ApiClient.create() val response = call(ApiClient.create())
val response = api.deleteRoom(propertyId, roomId)
if (response.isSuccessful) { if (response.isSuccessful) {
_state.update { it.copy(isLoading = false, success = true) } _state.update { it.copy(isLoading = false, success = true) }
onDone() onDone()
} else { } 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) { } 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 package com.android.trisolarispms.ui.room
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.core.ApiClient import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.core.viewmodel.launchRequest
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
class RoomListViewModel : ViewModel() { class RoomListViewModel : ViewModel() {
private val _state = MutableStateFlow(RoomListState()) private val _state = MutableStateFlow(RoomListState())
@@ -14,35 +13,35 @@ class RoomListViewModel : ViewModel() {
fun load(propertyId: String, showAll: Boolean = false, roomTypeCode: String? = null) { fun load(propertyId: String, showAll: Boolean = false, roomTypeCode: String? = null) {
if (propertyId.isBlank()) return if (propertyId.isBlank()) return
viewModelScope.launch { launchRequest(
_state.update { it.copy(isLoading = true, error = null, showAll = showAll, roomTypeCode = roomTypeCode) } state = _state,
try { setLoading = { it.copy(isLoading = true, error = null, showAll = showAll, roomTypeCode = roomTypeCode) },
val api = ApiClient.create() setError = { current, message -> current.copy(isLoading = false, error = message) },
val trimmedCode = roomTypeCode?.trim().orEmpty() defaultError = "Load failed"
val response = if (trimmedCode.isNotBlank()) { ) {
api.listRoomsByType( val api = ApiClient.create()
propertyId = propertyId, val trimmedCode = roomTypeCode?.trim().orEmpty()
roomTypeCode = trimmedCode, val response = if (trimmedCode.isNotBlank()) {
availableOnly = if (showAll) false else true api.listRoomsByType(
propertyId = propertyId,
roomTypeCode = trimmedCode,
availableOnly = !showAll
)
} else if (showAll) {
api.listRooms(propertyId)
} else {
api.listAvailableRooms(propertyId)
}
if (response.isSuccessful) {
_state.update {
it.copy(
isLoading = false,
rooms = response.body().orEmpty(),
error = null
) )
} else if (showAll) {
api.listRooms(propertyId)
} else {
api.listAvailableRooms(propertyId)
} }
if (response.isSuccessful) { } else {
_state.update { _state.update { it.copy(isLoading = false, error = "Load failed: ${response.code()}") }
it.copy(
isLoading = false,
rooms = response.body().orEmpty(),
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") }
} }
} }
} }

View File

@@ -1,34 +1,12 @@
package com.android.trisolarispms.ui.roomimage 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.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue 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 import androidx.lifecycle.viewmodel.compose.viewModel
@Composable @Composable
@OptIn(ExperimentalMaterial3Api::class)
fun AddImageTagScreen( fun AddImageTagScreen(
onBack: () -> Unit, onBack: () -> Unit,
onSave: () -> Unit, onSave: () -> Unit,
@@ -40,41 +18,12 @@ fun AddImageTagScreen(
viewModel.reset() viewModel.reset()
} }
Scaffold( ImageTagFormScreen(
topBar = { title = "Add Tag",
TopAppBar( name = state.name,
title = { Text("Add Tag") }, error = state.error,
navigationIcon = { onNameChange = viewModel::onNameChange,
IconButton(onClick = onBack) { onBack = onBack,
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") onSave = { viewModel.submitCreate(onSave) }
} )
},
actions = {
IconButton(onClick = { viewModel.submitCreate(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 = 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 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.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.android.trisolarispms.data.api.model.RoomImageTagDto import com.android.trisolarispms.data.api.model.RoomImageTagDto
@Composable @Composable
@OptIn(ExperimentalMaterial3Api::class)
fun EditImageTagScreen( fun EditImageTagScreen(
tag: RoomImageTagDto, tag: RoomImageTagDto,
onBack: () -> Unit, onBack: () -> Unit,
@@ -42,41 +20,12 @@ fun EditImageTagScreen(
viewModel.setTag(tag) viewModel.setTag(tag)
} }
Scaffold( ImageTagFormScreen(
topBar = { title = "Edit Tag",
TopAppBar( name = state.name,
title = { Text("Edit Tag") }, error = state.error,
navigationIcon = { onNameChange = viewModel::onNameChange,
IconButton(onClick = onBack) { onBack = onBack,
Icon(Icons.Default.ArrowBack, contentDescription = "Back") onSave = { viewModel.submitUpdate(tag.id.orEmpty(), onSave) }
} )
},
actions = {
IconButton(onClick = { viewModel.submitUpdate(tag.id.orEmpty(), 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 = 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.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.core.ApiClient 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 com.android.trisolarispms.data.api.model.RoomImageTagDto
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import retrofit2.Response
class ImageTagFormViewModel : ViewModel() { class ImageTagFormViewModel : ViewModel() {
private val _state = MutableStateFlow(ImageTagFormState()) private val _state = MutableStateFlow(ImageTagFormState())
@@ -26,47 +28,57 @@ class ImageTagFormViewModel : ViewModel() {
} }
fun submitCreate(onDone: () -> Unit) { fun submitCreate(onDone: () -> Unit) {
val name = state.value.name.trim() val payload = readValidatedPayload() ?: return
if (name.isBlank()) { submitMutation(
_state.update { it.copy(error = "Name is required") } action = "Create",
return onDone = onDone,
} resetOnSuccess = true
viewModelScope.launch { ) { api ->
_state.update { it.copy(isLoading = true, error = null) } api.createImageTag(payload)
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") }
}
} }
} }
fun submitUpdate(tagId: String, onDone: () -> Unit) { 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() val name = state.value.name.trim()
if (name.isBlank()) { if (name.isBlank()) {
_state.update { it.copy(error = "Name is required") } _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 { viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) } _state.update { it.copy(isLoading = true, error = null) }
try { try {
val api = ApiClient.create() val response = call(ApiClient.create())
val response = api.updateImageTag(tagId, RoomImageTagDto(name = name))
if (response.isSuccessful) { 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() onDone()
} else { } 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) { } 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,60 +1,59 @@
package com.android.trisolarispms.ui.roomimage package com.android.trisolarispms.ui.roomimage
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.core.ApiClient import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.core.viewmodel.launchRequest
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
class ImageTagViewModel : ViewModel() { class ImageTagViewModel : ViewModel() {
private val _state = MutableStateFlow(ImageTagState()) private val _state = MutableStateFlow(ImageTagState())
val state: StateFlow<ImageTagState> = _state val state: StateFlow<ImageTagState> = _state
fun load() { fun load() {
viewModelScope.launch { launchRequest(
_state.update { it.copy(isLoading = true, error = null) } state = _state,
try { setLoading = { it.copy(isLoading = true, error = null) },
val api = ApiClient.create() setError = { current, message -> current.copy(isLoading = false, error = message) },
val response = api.listImageTags() defaultError = "Load failed"
if (response.isSuccessful) { ) {
_state.update { val api = ApiClient.create()
it.copy( val response = api.listImageTags()
isLoading = false, if (response.isSuccessful) {
tags = response.body().orEmpty(), _state.update {
error = null it.copy(
) isLoading = false,
} tags = response.body().orEmpty(),
} else { error = null
_state.update { it.copy(isLoading = false, error = "Load failed: ${response.code()}") } )
} }
} catch (e: Exception) { } else {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Load failed") } _state.update { it.copy(isLoading = false, error = "Load failed: ${response.code()}") }
} }
} }
} }
fun delete(tagId: String) { fun delete(tagId: String) {
if (tagId.isBlank()) return if (tagId.isBlank()) return
viewModelScope.launch { launchRequest(
_state.update { it.copy(isLoading = true, error = null) } state = _state,
try { setLoading = { it.copy(isLoading = true, error = null) },
val api = ApiClient.create() setError = { current, message -> current.copy(isLoading = false, error = message) },
val response = api.deleteImageTag(tagId) defaultError = "Delete failed"
if (response.isSuccessful) { ) {
_state.update { current -> val api = ApiClient.create()
current.copy( val response = api.deleteImageTag(tagId)
isLoading = false, if (response.isSuccessful) {
tags = current.tags.filterNot { it.id == tagId }, _state.update { current ->
error = null current.copy(
) isLoading = false,
} tags = current.tags.filterNot { it.id == tagId },
} else { error = null
_state.update { it.copy(isLoading = false, error = "Delete failed: ${response.code()}") } )
} }
} catch (e: Exception) { } else {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Delete failed") } _state.update { it.copy(isLoading = false, error = "Delete failed: ${response.code()}") }
} }
} }
} }

View File

@@ -1,12 +1,11 @@
package com.android.trisolarispms.ui.roomstay package com.android.trisolarispms.ui.roomstay
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.core.ApiClient import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.core.viewmodel.launchRequest
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
class ActiveRoomStaysViewModel : ViewModel() { class ActiveRoomStaysViewModel : ViewModel() {
private val _state = MutableStateFlow(ActiveRoomStaysState()) private val _state = MutableStateFlow(ActiveRoomStaysState())
@@ -14,26 +13,26 @@ class ActiveRoomStaysViewModel : ViewModel() {
fun load(propertyId: String) { fun load(propertyId: String) {
if (propertyId.isBlank()) return if (propertyId.isBlank()) return
viewModelScope.launch { launchRequest(
_state.update { it.copy(isLoading = true, error = null) } state = _state,
try { setLoading = { it.copy(isLoading = true, error = null) },
val api = ApiClient.create() setError = { current, message -> current.copy(isLoading = false, error = message) },
val activeResponse = api.listActiveRoomStays(propertyId) defaultError = "Load failed"
val bookingsResponse = api.listBookings(propertyId, status = "CHECKED_IN") ) {
if (activeResponse.isSuccessful) { val api = ApiClient.create()
_state.update { val activeResponse = api.listActiveRoomStays(propertyId)
it.copy( val bookingsResponse = api.listBookings(propertyId, status = "CHECKED_IN")
isLoading = false, if (activeResponse.isSuccessful) {
items = activeResponse.body().orEmpty(), _state.update {
checkedInBookings = bookingsResponse.body().orEmpty(), it.copy(
error = null isLoading = false,
) items = activeResponse.body().orEmpty(),
} checkedInBookings = bookingsResponse.body().orEmpty(),
} else { error = null
_state.update { it.copy(isLoading = false, error = "Load failed: ${activeResponse.code()}") } )
} }
} catch (e: Exception) { } else {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Load failed") } _state.update { it.copy(isLoading = false, error = "Load failed: ${activeResponse.code()}") }
} }
} }
} }

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.ApiClient
import com.android.trisolarispms.data.api.core.ApiConstants import com.android.trisolarispms.data.api.core.ApiConstants
import com.android.trisolarispms.data.api.model.BookingDetailsResponse import com.android.trisolarispms.data.api.model.BookingDetailsResponse
import com.android.trisolarispms.core.viewmodel.launchRequest
import com.google.gson.Gson import com.google.gson.Gson
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import okhttp3.Request import okhttp3.Request
import okhttp3.sse.EventSource import okhttp3.sse.EventSource
import okhttp3.sse.EventSourceListener import okhttp3.sse.EventSourceListener
@@ -30,24 +31,24 @@ class BookingDetailsViewModel : ViewModel() {
fun load(propertyId: String, bookingId: String) { fun load(propertyId: String, bookingId: String) {
if (propertyId.isBlank() || bookingId.isBlank()) return if (propertyId.isBlank() || bookingId.isBlank()) return
viewModelScope.launch { launchRequest(
_state.update { it.copy(isLoading = true, error = null) } state = _state,
try { setLoading = { it.copy(isLoading = true, error = null) },
val api = ApiClient.create() setError = { current, message -> current.copy(isLoading = false, error = message) },
val response = api.getBookingDetails(propertyId, bookingId) defaultError = "Load failed"
if (response.isSuccessful) { ) {
_state.update { val api = ApiClient.create()
it.copy( val response = api.getBookingDetails(propertyId, bookingId)
isLoading = false, if (response.isSuccessful) {
details = response.body(), _state.update {
error = null it.copy(
) isLoading = false,
} details = response.body(),
} else { error = null
_state.update { it.copy(isLoading = false, error = "Load failed: ${response.code()}") } )
} }
} catch (e: Exception) { } else {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Load failed") } _state.update { it.copy(isLoading = false, error = "Load failed: ${response.code()}") }
} }
} }
} }

View File

@@ -1,12 +1,11 @@
package com.android.trisolarispms.ui.roomstay package com.android.trisolarispms.ui.roomstay
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.core.ApiClient import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.core.viewmodel.launchRequest
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
class BookingRoomStaysViewModel : ViewModel() { class BookingRoomStaysViewModel : ViewModel() {
private val _state = MutableStateFlow(BookingRoomStaysState()) private val _state = MutableStateFlow(BookingRoomStaysState())
@@ -18,25 +17,25 @@ class BookingRoomStaysViewModel : ViewModel() {
fun load(propertyId: String, bookingId: String) { fun load(propertyId: String, bookingId: String) {
if (propertyId.isBlank() || bookingId.isBlank()) return if (propertyId.isBlank() || bookingId.isBlank()) return
viewModelScope.launch { launchRequest(
_state.update { it.copy(isLoading = true, error = null) } state = _state,
try { setLoading = { it.copy(isLoading = true, error = null) },
val api = ApiClient.create() setError = { current, message -> current.copy(isLoading = false, error = message) },
val response = api.listActiveRoomStays(propertyId) defaultError = "Load failed"
if (response.isSuccessful) { ) {
val filtered = response.body().orEmpty().filter { it.bookingId == bookingId } val api = ApiClient.create()
_state.update { val response = api.listActiveRoomStays(propertyId)
it.copy( if (response.isSuccessful) {
isLoading = false, val filtered = response.body().orEmpty().filter { it.bookingId == bookingId }
stays = filtered, _state.update {
error = null it.copy(
) isLoading = false,
} stays = filtered,
} else { error = null
_state.update { it.copy(isLoading = false, error = "Load failed: ${response.code()}") } )
} }
} catch (e: Exception) { } else {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Load failed") } _state.update { it.copy(isLoading = false, error = "Load failed: ${response.code()}") }
} }
} }
} }

View File

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

View File

@@ -1,12 +1,11 @@
package com.android.trisolarispms.ui.roomstay package com.android.trisolarispms.ui.roomstay
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.core.ApiClient import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.core.viewmodel.launchRequest
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
class ManageRoomStaySelectViewModel : ViewModel() { class ManageRoomStaySelectViewModel : ViewModel() {
private val _state = MutableStateFlow(ManageRoomStaySelectState()) private val _state = MutableStateFlow(ManageRoomStaySelectState())
@@ -14,24 +13,24 @@ class ManageRoomStaySelectViewModel : ViewModel() {
fun load(propertyId: String, from: String, to: String) { fun load(propertyId: String, from: String, to: String) {
if (propertyId.isBlank() || from.isBlank() || to.isBlank()) return if (propertyId.isBlank() || from.isBlank() || to.isBlank()) return
viewModelScope.launch { launchRequest(
_state.update { it.copy(isLoading = true, error = null) } state = _state,
try { setLoading = { it.copy(isLoading = true, error = null) },
val api = ApiClient.create() setError = { current, message -> current.copy(isLoading = false, error = message) },
val response = api.listAvailableRoomsWithRate(propertyId, from = from, to = to) defaultError = "Load failed"
if (response.isSuccessful) { ) {
_state.update { val api = ApiClient.create()
it.copy( val response = api.listAvailableRoomsWithRate(propertyId, from = from, to = to)
isLoading = false, if (response.isSuccessful) {
rooms = response.body().orEmpty(), _state.update {
error = null it.copy(
) isLoading = false,
} rooms = response.body().orEmpty(),
} else { error = null
_state.update { it.copy(isLoading = false, error = "Load failed: ${response.code()}") } )
} }
} catch (e: Exception) { } else {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Load failed") } _state.update { it.copy(isLoading = false, error = "Load failed: ${response.code()}") }
} }
} }
} }

View File

@@ -10,13 +10,12 @@ fun AddAmenityScreen(
viewModel: AmenityFormViewModel = viewModel(), viewModel: AmenityFormViewModel = viewModel(),
amenityListViewModel: AmenityListViewModel = viewModel() amenityListViewModel: AmenityListViewModel = viewModel()
) { ) {
androidx.compose.runtime.LaunchedEffect(Unit) { AmenityEditorScreen(
viewModel.resetForm(preserveOptions = true)
}
AmenityFormScreen(
title = "Add Amenity", title = "Add Amenity",
setupKey = Unit,
onBack = onBack, onBack = onBack,
onSaveClick = { viewModel.submitCreate(onSave) }, onSaveClick = { viewModel.submitCreate(onSave) },
setupForm = { viewModel.resetForm(preserveOptions = true) },
viewModel = viewModel, viewModel = viewModel,
amenityListViewModel = amenityListViewModel 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.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.core.ApiClient 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.AmenityCreateRequest
import com.android.trisolarispms.data.api.model.AmenityUpdateRequest import com.android.trisolarispms.data.api.model.AmenityUpdateRequest
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import retrofit2.Response
class AmenityFormViewModel : ViewModel() { class AmenityFormViewModel : ViewModel() {
private val _state = MutableStateFlow(AmenityFormState()) private val _state = MutableStateFlow(AmenityFormState())
@@ -88,62 +90,75 @@ class AmenityFormViewModel : ViewModel() {
} }
fun submitCreate(onDone: () -> Unit) { fun submitCreate(onDone: () -> Unit) {
val name = state.value.name.trim() val payload = readValidatedPayload() ?: return
if (name.isBlank()) { submitMutation(
_state.update { it.copy(error = "Name is required") } action = "Create",
return onDone = onDone
} ) { api ->
viewModelScope.launch { api.createAmenity(payload.toCreateRequest())
_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") }
}
} }
} }
fun submitUpdate(amenityId: String, onDone: () -> Unit) { 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()) { if (name.isBlank()) {
_state.update { it.copy(error = "Name is required") } _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 { viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) } _state.update { it.copy(isLoading = true, error = null) }
try { try {
val iconKey = state.value.iconKey.trim().removeSuffix(".png").removeSuffix(".PNG") val response = call(ApiClient.create())
val api = ApiClient.create()
val response = api.updateAmenity(
amenityId,
AmenityUpdateRequest(
name = name,
category = state.value.category.trim().ifBlank { null },
iconKey = iconKey.ifBlank { null }
)
)
if (response.isSuccessful) { if (response.isSuccessful) {
_state.update { it.copy(isLoading = false, success = true) } _state.update { it.copy(isLoading = false, success = true) }
onDone() onDone()
} else { } 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) { } 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,60 +1,59 @@
package com.android.trisolarispms.ui.roomtype package com.android.trisolarispms.ui.roomtype
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.core.ApiClient import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.core.viewmodel.launchRequest
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
class AmenityListViewModel : ViewModel() { class AmenityListViewModel : ViewModel() {
private val _state = MutableStateFlow(AmenityListState()) private val _state = MutableStateFlow(AmenityListState())
val state: StateFlow<AmenityListState> = _state val state: StateFlow<AmenityListState> = _state
fun load() { fun load() {
viewModelScope.launch { launchRequest(
_state.update { it.copy(isLoading = true, error = null) } state = _state,
try { setLoading = { it.copy(isLoading = true, error = null) },
val api = ApiClient.create() setError = { current, message -> current.copy(isLoading = false, error = message) },
val response = api.listAmenities() defaultError = "Load failed"
if (response.isSuccessful) { ) {
_state.update { val api = ApiClient.create()
it.copy( val response = api.listAmenities()
isLoading = false, if (response.isSuccessful) {
items = response.body().orEmpty(), _state.update {
error = null it.copy(
) isLoading = false,
} items = response.body().orEmpty(),
} else { error = null
_state.update { it.copy(isLoading = false, error = "Load failed: ${response.code()}") } )
} }
} catch (e: Exception) { } else {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Load failed") } _state.update { it.copy(isLoading = false, error = "Load failed: ${response.code()}") }
} }
} }
} }
fun deleteAmenity(amenityId: String) { fun deleteAmenity(amenityId: String) {
if (amenityId.isBlank()) return if (amenityId.isBlank()) return
viewModelScope.launch { launchRequest(
_state.update { it.copy(isLoading = true, error = null) } state = _state,
try { setLoading = { it.copy(isLoading = true, error = null) },
val api = ApiClient.create() setError = { current, message -> current.copy(isLoading = false, error = message) },
val response = api.deleteAmenity(amenityId) defaultError = "Delete failed"
if (response.isSuccessful) { ) {
_state.update { current -> val api = ApiClient.create()
current.copy( val response = api.deleteAmenity(amenityId)
isLoading = false, if (response.isSuccessful) {
items = current.items.filterNot { it.id == amenityId }, _state.update { current ->
error = null current.copy(
) isLoading = false,
} items = current.items.filterNot { it.id == amenityId },
} else { error = null
_state.update { it.copy(isLoading = false, error = "Delete failed: ${response.code()}") } )
} }
} catch (e: Exception) { } else {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Delete failed") } _state.update { it.copy(isLoading = false, error = "Delete failed: ${response.code()}") }
} }
} }
} }

View File

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

View File

@@ -3,12 +3,14 @@ package com.android.trisolarispms.ui.roomtype
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.core.ApiClient 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.RoomTypeCreateRequest
import com.android.trisolarispms.data.api.model.RoomTypeUpdateRequest import com.android.trisolarispms.data.api.model.RoomTypeUpdateRequest
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import retrofit2.Response
class RoomTypeFormViewModel : ViewModel() { class RoomTypeFormViewModel : ViewModel() {
private val _state = MutableStateFlow(RoomTypeFormState()) 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 onAliasesChange(value: String) = _state.update { it.copy(otaAliases = value, error = null) }
fun submit(propertyId: String, onDone: () -> Unit) { fun submit(propertyId: String, onDone: () -> Unit) {
val code = state.value.code.trim() val payload = readValidatedPayload() ?: return
val name = state.value.name.trim() submitMutation(
if (code.isBlank() || name.isBlank()) { action = "Create",
_state.update { it.copy(error = "Code and name are required") } onDone = onDone
return ) { api ->
} api.createRoomType(propertyId, payload.toCreateRequest())
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") }
}
} }
} }
fun submitUpdate(propertyId: String, roomTypeId: String, onDone: () -> Unit) { fun submitUpdate(propertyId: String, roomTypeId: String, onDone: () -> Unit) {
val code = state.value.code.trim() val payload = readValidatedPayload() ?: return
val name = state.value.name.trim() submitMutation(
if (code.isBlank() || name.isBlank()) { action = "Update",
_state.update { it.copy(error = "Code and name are required") } onDone = onDone
return ) { api ->
} api.updateRoomType(propertyId, roomTypeId, payload.toUpdateRequest())
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") }
}
} }
} }
@@ -120,19 +74,88 @@ class RoomTypeFormViewModel : ViewModel() {
_state.update { it.copy(error = "Room type ID is missing") } _state.update { it.copy(error = "Room type ID is missing") }
return 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 { viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) } _state.update { it.copy(isLoading = true, error = null) }
try { try {
val api = ApiClient.create() val response = call(ApiClient.create())
val response = api.deleteRoomType(propertyId, roomTypeId)
if (response.isSuccessful) { if (response.isSuccessful) {
_state.update { it.copy(isLoading = false, success = true) } _state.update { it.copy(isLoading = false, success = true) }
onDone() onDone()
} else { } 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) { } 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.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.core.ApiClient 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.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
@@ -14,76 +17,81 @@ class RoomTypeListViewModel : ViewModel() {
fun load(propertyId: String) { fun load(propertyId: String) {
if (propertyId.isBlank()) return if (propertyId.isBlank()) return
viewModelScope.launch { launchRequest(
_state.update { it.copy(isLoading = true, error = null) } state = _state,
try { setLoading = { it.copy(isLoading = true, error = null) },
val api = ApiClient.create() setError = { current, message -> current.copy(isLoading = false, error = message) },
val response = api.listRoomTypes(propertyId) defaultError = "Load failed"
if (response.isSuccessful) { ) {
val items = response.body().orEmpty() val api = ApiClient.create()
_state.update { val response = api.listRoomTypes(propertyId)
it.copy( if (response.isSuccessful) {
isLoading = false, val items = response.body().orEmpty()
items = items, _state.update {
error = null it.copy(
) isLoading = false,
} items = items,
loadRoomTypeImages(propertyId, items) error = null
loadRoomTypeAvailableCounts(propertyId, items) )
} else {
_state.update { it.copy(isLoading = false, error = "Load failed: ${response.code()}") }
} }
} catch (e: Exception) { loadRoomTypeImages(propertyId, items)
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Load failed") } loadRoomTypeAvailableCounts(propertyId, items)
} else {
_state.update { it.copy(isLoading = false, error = "Load failed: ${response.code()}") }
} }
} }
} }
private fun loadRoomTypeImages(propertyId: String, items: List<com.android.trisolarispms.data.api.model.RoomTypeDto>) { private fun loadRoomTypeImages(propertyId: String, items: List<RoomTypeDto>) {
viewModelScope.launch { viewModelScope.launch {
val api = ApiClient.create() val updates = buildByTypeCodeMap(
val updates = mutableMapOf<String, com.android.trisolarispms.data.api.model.ImageDto?>() items = items,
for (item in items) { fetch = { api, code ->
val code = item.code?.trim().orEmpty()
if (code.isBlank()) continue
try {
val response = api.listRoomTypeImages(propertyId, code) val response = api.listRoomTypeImages(propertyId, code)
if (response.isSuccessful) { if (response.isSuccessful) response.body().orEmpty().firstOrNull() else null
updates[code] = response.body().orEmpty().firstOrNull()
}
} catch (_: Exception) {
// Ignore per-item failures to avoid blocking the list.
} }
} )
if (updates.isNotEmpty()) { if (updates.isNotEmpty()) {
_state.update { it.copy(imageByTypeCode = it.imageByTypeCode + updates) } _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 { viewModelScope.launch {
val api = ApiClient.create() val updates = buildByTypeCodeMap(
val updates = mutableMapOf<String, Int>() items = items,
for (item in items) { fetch = { api, code ->
val code = item.code?.trim().orEmpty()
if (code.isBlank()) continue
try {
val response = api.listRoomsByType( val response = api.listRoomsByType(
propertyId = propertyId, propertyId = propertyId,
roomTypeCode = code, roomTypeCode = code,
availableOnly = true availableOnly = true
) )
if (response.isSuccessful) { if (response.isSuccessful) {
updates[code] = response.body().orEmpty().size response.body().orEmpty().size
} else {
null
} }
} catch (_: Exception) {
// Ignore per-item failures.
} }
} )
if (updates.isNotEmpty()) { if (updates.isNotEmpty()) {
_state.update { it.copy(availableCountByTypeCode = it.availableCountByTypeCode + updates) } _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.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.core.ApiClient 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.PropertyAccessCodeCreateRequest
import com.android.trisolarispms.data.api.model.PropertyAccessCodeResponse 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.PropertyAccessCodeJoinRequest
import com.android.trisolarispms.data.api.model.UserRolesUpdateRequest import com.android.trisolarispms.data.api.model.UserRolesUpdateRequest
import com.android.trisolarispms.data.api.model.PropertyUserDisabledRequest import com.android.trisolarispms.data.api.model.PropertyUserDisabledRequest
@@ -28,33 +29,7 @@ class PropertyUsersViewModel : ViewModel() {
val state: StateFlow<PropertyUsersState> = _state val state: StateFlow<PropertyUsersState> = _state
fun loadAll(propertyId: String) { fun loadAll(propertyId: String) {
viewModelScope.launch { fetchUsers(propertyId = propertyId, phone = null, action = "Load")
_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") }
}
}
} }
fun searchUsers(propertyId: String, phoneInput: String) { 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) } _state.update { it.copy(users = emptyList(), error = null, isLoading = false, message = null) }
return return
} }
viewModelScope.launch { fetchUsers(propertyId = propertyId, phone = digits, action = "Search")
_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") }
}
}
} }
fun createAccessCode(propertyId: String, roles: List<String>) { fun createAccessCode(propertyId: String, roles: List<String>) {
@@ -97,27 +46,22 @@ class PropertyUsersViewModel : ViewModel() {
_state.update { it.copy(error = "Select at least one role", message = null) } _state.update { it.copy(error = "Select at least one role", message = null) }
return return
} }
viewModelScope.launch { runAction(defaultError = "Create failed") { api ->
_state.update { it.copy(isLoading = true, error = null, message = null) } val response = api.createAccessCode(
try { propertyId = propertyId,
val response = ApiClient.create().createAccessCode( body = PropertyAccessCodeCreateRequest(roles = roles)
propertyId = propertyId, )
body = PropertyAccessCodeCreateRequest(roles = roles) val body = response.body()
) if (response.isSuccessful && body != null) {
val body = response.body() _state.update {
if (response.isSuccessful && body != null) { it.copy(
_state.update { isLoading = false,
it.copy( accessCode = body,
isLoading = false, message = "Access code created"
accessCode = body, )
message = "Access code created"
)
}
} else {
_state.update { it.copy(isLoading = false, error = "Create failed: ${response.code()}") }
} }
} catch (e: Exception) { } else {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Create failed") } _state.update { it.copy(isLoading = false, error = "Create failed: ${response.code()}") }
} }
} }
} }
@@ -128,26 +72,21 @@ class PropertyUsersViewModel : ViewModel() {
_state.update { it.copy(error = "Code must be 6 digits", message = null) } _state.update { it.copy(error = "Code must be 6 digits", message = null) }
return return
} }
viewModelScope.launch { runAction(defaultError = "Join failed") { api ->
_state.update { it.copy(isLoading = true, error = null, message = null) } val response = api.joinAccessCode(
try { PropertyAccessCodeJoinRequest(propertyId = propertyId, code = digits)
val response = ApiClient.create().joinAccessCode( )
PropertyAccessCodeJoinRequest(propertyId = propertyId, code = digits) val body = response.body()
) if (response.isSuccessful && body != null) {
val body = response.body() _state.update {
if (response.isSuccessful && body != null) { it.copy(
_state.update { isLoading = false,
it.copy( message = "Joined property",
isLoading = false, error = null
message = "Joined property", )
error = null
)
}
} else {
_state.update { it.copy(isLoading = false, error = "Join failed: ${response.code()}") }
} }
} catch (e: Exception) { } else {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Join failed") } _state.update { it.copy(isLoading = false, error = "Join failed: ${response.code()}") }
} }
} }
} }
@@ -157,64 +96,50 @@ class PropertyUsersViewModel : ViewModel() {
_state.update { it.copy(error = "Select at least one role", message = null) } _state.update { it.copy(error = "Select at least one role", message = null) }
return return
} }
viewModelScope.launch { runAction(defaultError = "Update failed") { api ->
_state.update { it.copy(isLoading = true, error = null, message = null) } val response = api.updateUserRoles(
try { propertyId = propertyId,
val response = ApiClient.create().updateUserRoles( userId = userId,
propertyId = propertyId, body = UserRolesUpdateRequest(roles = roles)
userId = userId, )
body = UserRolesUpdateRequest(roles = roles) val body = response.body()
) if (response.isSuccessful && body != null) {
val body = response.body() _state.update { current ->
if (response.isSuccessful && body != null) { val updated = current.users.map { user ->
_state.update { current -> if (user.userId == userId) user.copy(roles = body.roles) else user
val updated = current.users.map { user ->
if (user.userId == userId) {
user.copy(roles = body.roles)
} else user
}
current.copy(
isLoading = false,
users = updated,
message = "Roles updated"
)
} }
} else { current.copy(
_state.update { it.copy(isLoading = false, error = "Update failed: ${response.code()}") } isLoading = false,
users = updated,
message = "Roles updated"
)
} }
} catch (e: Exception) { } else {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Update failed") } _state.update { it.copy(isLoading = false, error = "Update failed: ${response.code()}") }
} }
} }
} }
fun updateDisabled(propertyId: String, userId: String, disabled: Boolean) { fun updateDisabled(propertyId: String, userId: String, disabled: Boolean) {
viewModelScope.launch { runAction(defaultError = "Update failed") { api ->
_state.update { it.copy(isLoading = true, error = null, message = null) } val response = api.updateUserDisabled(
try { propertyId = propertyId,
val response = ApiClient.create().updateUserDisabled( userId = userId,
propertyId = propertyId, body = PropertyUserDisabledRequest(disabled = disabled)
userId = userId, )
body = PropertyUserDisabledRequest(disabled = disabled) if (response.isSuccessful) {
) _state.update { current ->
if (response.isSuccessful) { val updated = current.users.map { user ->
_state.update { current -> if (user.userId == userId) user.copy(disabled = disabled) else user
val updated = current.users.map { user ->
if (user.userId == userId) {
user.copy(disabled = disabled)
} else user
}
current.copy(
isLoading = false,
users = updated,
message = if (disabled) "User disabled" else "User enabled"
)
} }
} else { current.copy(
_state.update { it.copy(isLoading = false, error = "Update failed: ${response.code()}") } isLoading = false,
users = updated,
message = if (disabled) "User disabled" else "User enabled"
)
} }
} catch (e: Exception) { } else {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Update failed") } _state.update { it.copy(isLoading = false, error = "Update failed: ${response.code()}") }
} }
} }
} }
@@ -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.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.core.ApiClient 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.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
@@ -25,37 +27,11 @@ class UserDirectoryViewModel : ViewModel() {
val state: StateFlow<UserDirectoryState> = _state val state: StateFlow<UserDirectoryState> = _state
fun loadAll(mode: UserDirectoryMode) { fun loadAll(mode: UserDirectoryMode) {
viewModelScope.launch { runDirectoryQuery(
_state.update { it.copy(isLoading = true, error = null) } mode = mode,
try { phone = null,
val api = ApiClient.create() action = "Load"
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
)
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) { fun search(mode: UserDirectoryMode, phoneInput: String) {
@@ -64,39 +40,65 @@ class UserDirectoryViewModel : ViewModel() {
_state.update { it.copy(users = emptyList(), error = null, isLoading = false) } _state.update { it.copy(users = emptyList(), error = null, isLoading = false) }
return return
} }
viewModelScope.launch { runDirectoryQuery(
_state.update { it.copy(isLoading = true, error = null) } mode = mode,
try { phone = digits,
val api = ApiClient.create() action = "Search"
when (mode) { )
UserDirectoryMode.SuperAdmin -> { }
val response = api.listUsers(digits)
val users = response.body()?.toSuperAdminUsers() private data class UserQueryResult(
if (response.isSuccessful && users != null) { val users: List<PropertyUserUi>?,
_state.update { it.copy(isLoading = false, users = users, error = null) } val code: Int,
} else { val isSuccessful: Boolean
_state.update { it.copy(isLoading = false, error = "Search failed: ${response.code()}") } )
}
} private fun runDirectoryQuery(
is UserDirectoryMode.Property -> { mode: UserDirectoryMode,
val response = api.searchPropertyUsers( phone: String?,
propertyId = mode.propertyId, action: String
phone = digits ) {
) launchRequest(
val users = response.body()?.toPropertyUsers() state = _state,
if (response.isSuccessful && users != null) { setLoading = { it.copy(isLoading = true, error = null) },
_state.update { it.copy(isLoading = false, users = users, error = null) } setError = { current, message -> current.copy(isLoading = false, error = message) },
} else { defaultError = "$action failed"
_state.update { it.copy(isLoading = false, error = "Search failed: ${response.code()}") } ) {
} val result = queryUsers(ApiClient.create(), mode, phone)
} if (result.isSuccessful && result.users != null) {
} _state.update { it.copy(isLoading = false, users = result.users, error = null) }
} catch (e: Exception) { } else {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Search failed") } _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 = phone
)
UserQueryResult(
users = response.body()?.toPropertyUsers(),
code = response.code(),
isSuccessful = response.isSuccessful
)
}
}
private fun List<com.android.trisolarispms.data.api.model.AppUserSummaryResponse>.toSuperAdminUsers(): private fun List<com.android.trisolarispms.data.api.model.AppUserSummaryResponse>.toSuperAdminUsers():
List<PropertyUserUi> = List<PropertyUserUi> =
map { map {