ai removed boilerplate and orgnized code even more

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

View File

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

View File

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

View File

@@ -1,15 +1,14 @@
package com.android.trisolarispms.ui.home
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.core.auth.Role
import com.android.trisolarispms.core.auth.toRoles
import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.data.api.model.PropertyAccessCodeJoinRequest
import com.android.trisolarispms.core.viewmodel.launchRequest
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
data class HomeJoinPropertyState(
val isLoading: Boolean = false,
@@ -34,31 +33,31 @@ class HomeJoinPropertyViewModel : ViewModel() {
_state.update { it.copy(error = "Code must be 6 digits", message = null) }
return
}
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null, message = null) }
try {
val response = ApiClient.create().joinAccessCode(
PropertyAccessCodeJoinRequest(
propertyId = trimmedPropertyId,
code = digits
)
launchRequest(
state = _state,
setLoading = { it.copy(isLoading = true, error = null, message = null) },
setError = { current, message -> current.copy(isLoading = false, error = message, message = null) },
defaultError = "Join failed"
) {
val response = ApiClient.create().joinAccessCode(
PropertyAccessCodeJoinRequest(
propertyId = trimmedPropertyId,
code = digits
)
val body = response.body()
if (response.isSuccessful && body != null) {
_state.update {
it.copy(
isLoading = false,
message = "Joined property",
error = null,
joinedPropertyId = body.propertyId ?: trimmedPropertyId,
joinedRoles = body.roles.toRoles()
)
}
} else {
_state.update { it.copy(isLoading = false, error = "Join failed: ${response.code()}") }
)
val body = response.body()
if (response.isSuccessful && body != null) {
_state.update {
it.copy(
isLoading = false,
message = "Joined property",
error = null,
joinedPropertyId = body.propertyId ?: trimmedPropertyId,
joinedRoles = body.roles.toRoles()
)
}
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Join failed") }
} else {
_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.RazorpaySettingsScreen
import com.android.trisolarispms.ui.roomstay.ActiveRoomStaysScreen
import com.android.trisolarispms.ui.roomstay.ManageRoomStaySelection
import com.android.trisolarispms.ui.roomstay.ManageRoomStayRatesScreen
import com.android.trisolarispms.ui.roomstay.ManageRoomStaySelectScreen
@@ -18,23 +19,56 @@ internal fun renderStayFlowRoutes(
authViewModel: AuthViewModel,
authz: AuthzPolicy
): Boolean {
@Composable
fun renderManageRoomStaySelectRoute(
propertyId: String,
fromAt: String,
toAt: String?,
onNext: (List<ManageRoomStaySelection>) -> Unit
) {
ManageRoomStaySelectScreen(
propertyId = propertyId,
bookingFromAt = fromAt,
bookingToAt = toAt,
onBack = { refs.openActiveRoomStays(propertyId) },
onNext = { rooms ->
refs.selectedManageRooms.value = rooms
onNext(rooms)
}
)
}
@Composable
fun renderManageRoomStayRatesRoute(
propertyId: String,
bookingId: String,
fromAt: String,
toAt: String?,
onBack: () -> Unit,
onDone: () -> Unit
) {
ManageRoomStayRatesScreen(
propertyId = propertyId,
bookingId = bookingId,
checkInAt = fromAt,
checkOutAt = toAt,
selectedRooms = refs.selectedManageRooms.value,
onBack = onBack,
onDone = onDone
)
}
when (val currentRoute = refs.currentRoute) {
is AppRoute.ActiveRoomStays -> ActiveRoomStaysScreen(
propertyId = currentRoute.propertyId,
propertyName = currentRoute.propertyName,
onBack = {
val blockBack = authz.shouldBlockBackToHome(
propertyId = currentRoute.propertyId,
propertyCount = state.propertyRoles.size
)
val blockBack = shouldBlockHomeBack(authz, state, currentRoute.propertyId)
if (!blockBack) {
refs.route.value = AppRoute.Home
}
},
showBack = !authz.shouldBlockBackToHome(
propertyId = currentRoute.propertyId,
propertyCount = state.propertyRoles.size
),
showBack = !shouldBlockHomeBack(authz, state, currentRoute.propertyId),
onViewRooms = { refs.route.value = AppRoute.Rooms(currentRoute.propertyId) },
onCreateBooking = { refs.route.value = AppRoute.CreateBooking(currentRoute.propertyId) },
canCreateBooking = authz.canCreateBookingFor(currentRoute.propertyId),
@@ -91,62 +125,54 @@ internal fun renderStayFlowRoutes(
}
)
is AppRoute.ManageRoomStaySelect -> ManageRoomStaySelectScreen(
is AppRoute.ManageRoomStaySelect -> renderManageRoomStaySelectRoute(
propertyId = currentRoute.propertyId,
bookingFromAt = currentRoute.fromAt,
bookingToAt = currentRoute.toAt,
onBack = { refs.openActiveRoomStays(currentRoute.propertyId) },
onNext = { rooms ->
refs.selectedManageRooms.value = rooms
refs.route.value = AppRoute.ManageRoomStayRates(
propertyId = currentRoute.propertyId,
bookingId = currentRoute.bookingId,
fromAt = currentRoute.fromAt,
toAt = currentRoute.toAt
)
}
)
fromAt = currentRoute.fromAt,
toAt = currentRoute.toAt
) {
refs.route.value = AppRoute.ManageRoomStayRates(
propertyId = currentRoute.propertyId,
bookingId = currentRoute.bookingId,
fromAt = currentRoute.fromAt,
toAt = currentRoute.toAt
)
}
is AppRoute.ManageRoomStaySelectFromBooking -> ManageRoomStaySelectScreen(
is AppRoute.ManageRoomStaySelectFromBooking -> renderManageRoomStaySelectRoute(
propertyId = currentRoute.propertyId,
bookingFromAt = currentRoute.fromAt,
bookingToAt = currentRoute.toAt,
onBack = { refs.openActiveRoomStays(currentRoute.propertyId) },
onNext = { rooms ->
refs.selectedManageRooms.value = rooms
refs.route.value = AppRoute.ManageRoomStayRatesFromBooking(
propertyId = currentRoute.propertyId,
bookingId = currentRoute.bookingId,
guestId = currentRoute.guestId,
fromAt = currentRoute.fromAt,
toAt = currentRoute.toAt
)
}
)
fromAt = currentRoute.fromAt,
toAt = currentRoute.toAt
) {
refs.route.value = AppRoute.ManageRoomStayRatesFromBooking(
propertyId = currentRoute.propertyId,
bookingId = currentRoute.bookingId,
guestId = currentRoute.guestId,
fromAt = currentRoute.fromAt,
toAt = currentRoute.toAt
)
}
is AppRoute.ManageRoomStayRates -> ManageRoomStayRatesScreen(
is AppRoute.ManageRoomStayRates -> renderManageRoomStayRatesRoute(
propertyId = currentRoute.propertyId,
bookingId = currentRoute.bookingId,
checkInAt = currentRoute.fromAt,
checkOutAt = currentRoute.toAt,
selectedRooms = refs.selectedManageRooms.value,
fromAt = currentRoute.fromAt,
toAt = currentRoute.toAt,
onBack = {
refs.route.value = AppRoute.ManageRoomStaySelect(
currentRoute.propertyId,
currentRoute.bookingId,
currentRoute.fromAt,
currentRoute.toAt
propertyId = currentRoute.propertyId,
bookingId = currentRoute.bookingId,
fromAt = currentRoute.fromAt,
toAt = currentRoute.toAt
)
},
onDone = { refs.openActiveRoomStays(currentRoute.propertyId) }
)
is AppRoute.ManageRoomStayRatesFromBooking -> ManageRoomStayRatesScreen(
is AppRoute.ManageRoomStayRatesFromBooking -> renderManageRoomStayRatesRoute(
propertyId = currentRoute.propertyId,
bookingId = currentRoute.bookingId,
checkInAt = currentRoute.fromAt,
checkOutAt = currentRoute.toAt,
selectedRooms = refs.selectedManageRooms.value,
fromAt = currentRoute.fromAt,
toAt = currentRoute.toAt,
onBack = {
refs.route.value = AppRoute.ManageRoomStaySelectFromBooking(
currentRoute.propertyId,
@@ -169,3 +195,9 @@ internal fun renderStayFlowRoutes(
}
return true
}
private fun shouldBlockHomeBack(authz: AuthzPolicy, state: AuthUiState, propertyId: String): Boolean =
authz.shouldBlockBackToHome(
propertyId = propertyId,
propertyCount = state.propertyRoles.size
)

View File

@@ -3,49 +3,33 @@ package com.android.trisolarispms.ui.payment
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.data.api.core.ApiService
import com.android.trisolarispms.data.api.model.RazorpayRefundRequest
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import retrofit2.Response
class BookingPaymentsViewModel : ViewModel() {
private val _state = MutableStateFlow(BookingPaymentsState())
val state: StateFlow<BookingPaymentsState> = _state
fun load(propertyId: String, bookingId: String) {
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null, message = null) }
try {
val api = ApiClient.create()
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) {
runPaymentAction(defaultError = "Load failed") { api ->
val response = api.listPayments(propertyId, bookingId)
val body = response.body()
if (response.isSuccessful && body != null) {
_state.update {
it.copy(
isLoading = false,
error = e.localizedMessage ?: "Load failed",
payments = body,
error = 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) }
return
}
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null, message = null) }
try {
val api = ApiClient.create()
val response = api.createPayment(
propertyId = propertyId,
bookingId = bookingId,
body = com.android.trisolarispms.data.api.model.PaymentCreateRequest(amount = amount)
)
val body = response.body()
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(
runPaymentAction(defaultError = "Create failed") { api ->
val response = api.createPayment(
propertyId = propertyId,
bookingId = bookingId,
body = com.android.trisolarispms.data.api.model.PaymentCreateRequest(amount = amount)
)
val body = response.body()
if (response.isSuccessful && body != null) {
_state.update { current ->
current.copy(
isLoading = false,
error = e.localizedMessage ?: "Create failed",
message = null
payments = listOf(body) + current.payments,
error = null,
message = "Cash payment added"
)
}
} else {
setActionFailure("Create", response)
}
}
}
fun deleteCashPayment(propertyId: String, bookingId: String, paymentId: String) {
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null, message = null) }
try {
val api = ApiClient.create()
val response = api.deletePayment(
propertyId = propertyId,
bookingId = bookingId,
paymentId = paymentId
)
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(
runPaymentAction(defaultError = "Delete failed") { api ->
val response = api.deletePayment(
propertyId = propertyId,
bookingId = bookingId,
paymentId = paymentId
)
if (response.isSuccessful) {
_state.update { current ->
current.copy(
isLoading = false,
error = e.localizedMessage ?: "Delete failed",
message = null
payments = current.payments.filterNot { it.id == paymentId },
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) }
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 {
_state.update { it.copy(isLoading = true, error = null, message = null) }
try {
val api = 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
)
}
}
block(ApiClient.create())
} catch (e: Exception) {
_state.update {
it.copy(
isLoading = false,
error = e.localizedMessage ?: "Refund failed",
error = e.localizedMessage ?: defaultError,
message = null
)
}
}
}
}
private fun setActionFailure(action: String, response: Response<*>) {
_state.update {
it.copy(
isLoading = false,
error = "$action failed: ${response.code()}",
message = null
)
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,60 +1,59 @@
package com.android.trisolarispms.ui.roomimage
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.core.viewmodel.launchRequest
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
class ImageTagViewModel : ViewModel() {
private val _state = MutableStateFlow(ImageTagState())
val state: StateFlow<ImageTagState> = _state
fun load() {
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) }
try {
val api = ApiClient.create()
val response = api.listImageTags()
if (response.isSuccessful) {
_state.update {
it.copy(
isLoading = false,
tags = response.body().orEmpty(),
error = null
)
}
} else {
_state.update { it.copy(isLoading = false, error = "Load failed: ${response.code()}") }
launchRequest(
state = _state,
setLoading = { it.copy(isLoading = true, error = null) },
setError = { current, message -> current.copy(isLoading = false, error = message) },
defaultError = "Load failed"
) {
val api = ApiClient.create()
val response = api.listImageTags()
if (response.isSuccessful) {
_state.update {
it.copy(
isLoading = false,
tags = response.body().orEmpty(),
error = null
)
}
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Load failed") }
} else {
_state.update { it.copy(isLoading = false, error = "Load failed: ${response.code()}") }
}
}
}
fun delete(tagId: String) {
if (tagId.isBlank()) return
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) }
try {
val api = ApiClient.create()
val response = api.deleteImageTag(tagId)
if (response.isSuccessful) {
_state.update { current ->
current.copy(
isLoading = false,
tags = current.tags.filterNot { it.id == tagId },
error = null
)
}
} else {
_state.update { it.copy(isLoading = false, error = "Delete failed: ${response.code()}") }
launchRequest(
state = _state,
setLoading = { it.copy(isLoading = true, error = null) },
setError = { current, message -> current.copy(isLoading = false, error = message) },
defaultError = "Delete failed"
) {
val api = ApiClient.create()
val response = api.deleteImageTag(tagId)
if (response.isSuccessful) {
_state.update { current ->
current.copy(
isLoading = false,
tags = current.tags.filterNot { it.id == tagId },
error = null
)
}
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Delete failed") }
} else {
_state.update { it.copy(isLoading = false, error = "Delete failed: ${response.code()}") }
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,60 +1,59 @@
package com.android.trisolarispms.ui.roomtype
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.core.viewmodel.launchRequest
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
class AmenityListViewModel : ViewModel() {
private val _state = MutableStateFlow(AmenityListState())
val state: StateFlow<AmenityListState> = _state
fun load() {
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) }
try {
val api = ApiClient.create()
val response = api.listAmenities()
if (response.isSuccessful) {
_state.update {
it.copy(
isLoading = false,
items = response.body().orEmpty(),
error = null
)
}
} else {
_state.update { it.copy(isLoading = false, error = "Load failed: ${response.code()}") }
launchRequest(
state = _state,
setLoading = { it.copy(isLoading = true, error = null) },
setError = { current, message -> current.copy(isLoading = false, error = message) },
defaultError = "Load failed"
) {
val api = ApiClient.create()
val response = api.listAmenities()
if (response.isSuccessful) {
_state.update {
it.copy(
isLoading = false,
items = response.body().orEmpty(),
error = null
)
}
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Load failed") }
} else {
_state.update { it.copy(isLoading = false, error = "Load failed: ${response.code()}") }
}
}
}
fun deleteAmenity(amenityId: String) {
if (amenityId.isBlank()) return
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) }
try {
val api = ApiClient.create()
val response = api.deleteAmenity(amenityId)
if (response.isSuccessful) {
_state.update { current ->
current.copy(
isLoading = false,
items = current.items.filterNot { it.id == amenityId },
error = null
)
}
} else {
_state.update { it.copy(isLoading = false, error = "Delete failed: ${response.code()}") }
launchRequest(
state = _state,
setLoading = { it.copy(isLoading = true, error = null) },
setError = { current, message -> current.copy(isLoading = false, error = message) },
defaultError = "Delete failed"
) {
val api = ApiClient.create()
val response = api.deleteAmenity(amenityId)
if (response.isSuccessful) {
_state.update { current ->
current.copy(
isLoading = false,
items = current.items.filterNot { it.id == amenityId },
error = null
)
}
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Delete failed") }
} else {
_state.update { it.copy(isLoading = false, error = "Delete failed: ${response.code()}") }
}
}
}

View File

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

View File

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

View File

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

View File

@@ -3,9 +3,10 @@ package com.android.trisolarispms.ui.users
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.data.api.core.ApiService
import com.android.trisolarispms.data.api.model.PropertyAccessCodeCreateRequest
import com.android.trisolarispms.data.api.model.PropertyAccessCodeResponse
import com.android.trisolarispms.data.api.model.PropertyUserResponse
import com.android.trisolarispms.data.api.model.PropertyUserDetailsResponse
import com.android.trisolarispms.data.api.model.PropertyAccessCodeJoinRequest
import com.android.trisolarispms.data.api.model.UserRolesUpdateRequest
import com.android.trisolarispms.data.api.model.PropertyUserDisabledRequest
@@ -28,33 +29,7 @@ class PropertyUsersViewModel : ViewModel() {
val state: StateFlow<PropertyUsersState> = _state
fun loadAll(propertyId: String) {
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null, message = null) }
try {
val response = ApiClient.create().searchPropertyUsers(
propertyId = propertyId,
phone = null
)
val body = response.body()
if (response.isSuccessful && body != null) {
val mapped = body.map {
PropertyUserUi(
userId = it.userId,
roles = it.roles,
name = it.name,
phoneE164 = it.phoneE164,
disabled = it.disabled,
superAdmin = it.superAdmin
)
}
_state.update { it.copy(isLoading = false, users = mapped, error = null) }
} else {
_state.update { it.copy(isLoading = false, error = "Load failed: ${response.code()}") }
}
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Load failed") }
}
}
fetchUsers(propertyId = propertyId, phone = null, action = "Load")
}
fun searchUsers(propertyId: String, phoneInput: String) {
@@ -63,33 +38,7 @@ class PropertyUsersViewModel : ViewModel() {
_state.update { it.copy(users = emptyList(), error = null, isLoading = false, message = null) }
return
}
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null, message = null) }
try {
val response = ApiClient.create().searchPropertyUsers(
propertyId = propertyId,
phone = digits
)
val body = response.body()
if (response.isSuccessful && body != null) {
val mapped = body.map {
PropertyUserUi(
userId = it.userId,
roles = it.roles,
name = it.name,
phoneE164 = it.phoneE164,
disabled = it.disabled,
superAdmin = it.superAdmin
)
}
_state.update { it.copy(isLoading = false, users = mapped, error = null) }
} else {
_state.update { it.copy(isLoading = false, error = "Search failed: ${response.code()}") }
}
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Search failed") }
}
}
fetchUsers(propertyId = propertyId, phone = digits, action = "Search")
}
fun createAccessCode(propertyId: String, roles: List<String>) {
@@ -97,27 +46,22 @@ class PropertyUsersViewModel : ViewModel() {
_state.update { it.copy(error = "Select at least one role", message = null) }
return
}
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null, message = null) }
try {
val response = ApiClient.create().createAccessCode(
propertyId = propertyId,
body = PropertyAccessCodeCreateRequest(roles = roles)
)
val body = response.body()
if (response.isSuccessful && body != null) {
_state.update {
it.copy(
isLoading = false,
accessCode = body,
message = "Access code created"
)
}
} else {
_state.update { it.copy(isLoading = false, error = "Create failed: ${response.code()}") }
runAction(defaultError = "Create failed") { api ->
val response = api.createAccessCode(
propertyId = propertyId,
body = PropertyAccessCodeCreateRequest(roles = roles)
)
val body = response.body()
if (response.isSuccessful && body != null) {
_state.update {
it.copy(
isLoading = false,
accessCode = body,
message = "Access code created"
)
}
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Create failed") }
} else {
_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) }
return
}
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null, message = null) }
try {
val response = ApiClient.create().joinAccessCode(
PropertyAccessCodeJoinRequest(propertyId = propertyId, code = digits)
)
val body = response.body()
if (response.isSuccessful && body != null) {
_state.update {
it.copy(
isLoading = false,
message = "Joined property",
error = null
)
}
} else {
_state.update { it.copy(isLoading = false, error = "Join failed: ${response.code()}") }
runAction(defaultError = "Join failed") { api ->
val response = api.joinAccessCode(
PropertyAccessCodeJoinRequest(propertyId = propertyId, code = digits)
)
val body = response.body()
if (response.isSuccessful && body != null) {
_state.update {
it.copy(
isLoading = false,
message = "Joined property",
error = null
)
}
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Join failed") }
} else {
_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) }
return
}
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null, message = null) }
try {
val response = ApiClient.create().updateUserRoles(
propertyId = propertyId,
userId = userId,
body = UserRolesUpdateRequest(roles = roles)
)
val body = response.body()
if (response.isSuccessful && body != null) {
_state.update { current ->
val updated = current.users.map { user ->
if (user.userId == userId) {
user.copy(roles = body.roles)
} else user
}
current.copy(
isLoading = false,
users = updated,
message = "Roles updated"
)
runAction(defaultError = "Update failed") { api ->
val response = api.updateUserRoles(
propertyId = propertyId,
userId = userId,
body = UserRolesUpdateRequest(roles = roles)
)
val body = response.body()
if (response.isSuccessful && body != null) {
_state.update { current ->
val updated = current.users.map { user ->
if (user.userId == userId) user.copy(roles = body.roles) else user
}
} else {
_state.update { it.copy(isLoading = false, error = "Update failed: ${response.code()}") }
current.copy(
isLoading = false,
users = updated,
message = "Roles updated"
)
}
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Update failed") }
} else {
_state.update { it.copy(isLoading = false, error = "Update failed: ${response.code()}") }
}
}
}
fun updateDisabled(propertyId: String, userId: String, disabled: Boolean) {
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null, message = null) }
try {
val response = ApiClient.create().updateUserDisabled(
propertyId = propertyId,
userId = userId,
body = PropertyUserDisabledRequest(disabled = disabled)
)
if (response.isSuccessful) {
_state.update { current ->
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"
)
runAction(defaultError = "Update failed") { api ->
val response = api.updateUserDisabled(
propertyId = propertyId,
userId = userId,
body = PropertyUserDisabledRequest(disabled = disabled)
)
if (response.isSuccessful) {
_state.update { current ->
val updated = current.users.map { user ->
if (user.userId == userId) user.copy(disabled = disabled) else user
}
} else {
_state.update { it.copy(isLoading = false, error = "Update failed: ${response.code()}") }
current.copy(
isLoading = false,
users = updated,
message = if (disabled) "User disabled" else "User enabled"
)
}
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Update failed") }
} else {
_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.viewModelScope
import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.data.api.core.ApiService
import com.android.trisolarispms.core.viewmodel.launchRequest
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
@@ -25,37 +27,11 @@ class UserDirectoryViewModel : ViewModel() {
val state: StateFlow<UserDirectoryState> = _state
fun loadAll(mode: UserDirectoryMode) {
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) }
try {
val api = ApiClient.create()
when (mode) {
UserDirectoryMode.SuperAdmin -> {
val response = api.listUsers(null)
val users = response.body()?.toSuperAdminUsers()
if (response.isSuccessful && users != null) {
_state.update { it.copy(isLoading = false, users = users, error = null) }
} else {
_state.update { it.copy(isLoading = false, error = "Load failed: ${response.code()}") }
}
}
is UserDirectoryMode.Property -> {
val response = api.searchPropertyUsers(
propertyId = mode.propertyId,
phone = null
)
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") }
}
}
runDirectoryQuery(
mode = mode,
phone = null,
action = "Load"
)
}
fun search(mode: UserDirectoryMode, phoneInput: String) {
@@ -64,39 +40,65 @@ class UserDirectoryViewModel : ViewModel() {
_state.update { it.copy(users = emptyList(), error = null, isLoading = false) }
return
}
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) }
try {
val api = ApiClient.create()
when (mode) {
UserDirectoryMode.SuperAdmin -> {
val response = api.listUsers(digits)
val users = response.body()?.toSuperAdminUsers()
if (response.isSuccessful && users != null) {
_state.update { it.copy(isLoading = false, users = users, error = null) }
} else {
_state.update { it.copy(isLoading = false, error = "Search failed: ${response.code()}") }
}
}
is UserDirectoryMode.Property -> {
val response = api.searchPropertyUsers(
propertyId = mode.propertyId,
phone = digits
)
val users = response.body()?.toPropertyUsers()
if (response.isSuccessful && users != null) {
_state.update { it.copy(isLoading = false, users = users, error = null) }
} else {
_state.update { it.copy(isLoading = false, error = "Search failed: ${response.code()}") }
}
}
}
} catch (e: Exception) {
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Search failed") }
runDirectoryQuery(
mode = mode,
phone = digits,
action = "Search"
)
}
private data class UserQueryResult(
val users: List<PropertyUserUi>?,
val code: Int,
val isSuccessful: Boolean
)
private fun runDirectoryQuery(
mode: UserDirectoryMode,
phone: String?,
action: String
) {
launchRequest(
state = _state,
setLoading = { it.copy(isLoading = true, error = null) },
setError = { current, message -> current.copy(isLoading = false, error = message) },
defaultError = "$action failed"
) {
val result = queryUsers(ApiClient.create(), mode, phone)
if (result.isSuccessful && result.users != null) {
_state.update { it.copy(isLoading = false, users = result.users, error = null) }
} else {
_state.update { it.copy(isLoading = false, error = "$action failed: ${result.code}") }
}
}
}
private suspend fun queryUsers(
api: ApiService,
mode: UserDirectoryMode,
phone: String?
): UserQueryResult = when (mode) {
UserDirectoryMode.SuperAdmin -> {
val response = api.listUsers(phone)
UserQueryResult(
users = response.body()?.toSuperAdminUsers(),
code = response.code(),
isSuccessful = response.isSuccessful
)
}
is UserDirectoryMode.Property -> {
val response = api.searchPropertyUsers(
propertyId = mode.propertyId,
phone = phone
)
UserQueryResult(
users = response.body()?.toPropertyUsers(),
code = response.code(),
isSuccessful = response.isSuccessful
)
}
}
private fun List<com.android.trisolarispms.data.api.model.AppUserSummaryResponse>.toSuperAdminUsers():
List<PropertyUserUi> =
map {