Add amenities support for room types
This commit is contained in:
@@ -19,7 +19,11 @@ import com.android.trisolarispms.ui.property.AddPropertyScreen
|
|||||||
import com.android.trisolarispms.ui.room.RoomFormScreen
|
import com.android.trisolarispms.ui.room.RoomFormScreen
|
||||||
import com.android.trisolarispms.ui.room.RoomsScreen
|
import com.android.trisolarispms.ui.room.RoomsScreen
|
||||||
import com.android.trisolarispms.ui.roomstay.ActiveRoomStaysScreen
|
import com.android.trisolarispms.ui.roomstay.ActiveRoomStaysScreen
|
||||||
|
import com.android.trisolarispms.ui.roomtype.AddAmenityScreen
|
||||||
import com.android.trisolarispms.ui.roomtype.AddRoomTypeScreen
|
import com.android.trisolarispms.ui.roomtype.AddRoomTypeScreen
|
||||||
|
import com.android.trisolarispms.ui.roomtype.AmenitiesScreen
|
||||||
|
import com.android.trisolarispms.ui.roomtype.EditAmenityScreen
|
||||||
|
import com.android.trisolarispms.ui.roomtype.EditRoomTypeScreen
|
||||||
import com.android.trisolarispms.ui.roomtype.RoomTypesScreen
|
import com.android.trisolarispms.ui.roomtype.RoomTypesScreen
|
||||||
import com.android.trisolarispms.ui.theme.TrisolarisPMSTheme
|
import com.android.trisolarispms.ui.theme.TrisolarisPMSTheme
|
||||||
|
|
||||||
@@ -45,8 +49,13 @@ class MainActivity : ComponentActivity() {
|
|||||||
val selectedPropertyId = remember { mutableStateOf<String?>(null) }
|
val selectedPropertyId = remember { mutableStateOf<String?>(null) }
|
||||||
val selectedPropertyName = remember { mutableStateOf<String?>(null) }
|
val selectedPropertyName = remember { mutableStateOf<String?>(null) }
|
||||||
val selectedRoom = remember { mutableStateOf<com.android.trisolarispms.data.api.model.RoomDto?>(null) }
|
val selectedRoom = remember { mutableStateOf<com.android.trisolarispms.data.api.model.RoomDto?>(null) }
|
||||||
|
val selectedRoomType = remember { mutableStateOf<com.android.trisolarispms.data.api.model.RoomTypeDto?>(null) }
|
||||||
|
val selectedAmenity = remember { mutableStateOf<com.android.trisolarispms.data.api.model.AmenityDto?>(null) }
|
||||||
val roomFormKey = remember { mutableStateOf(0) }
|
val roomFormKey = remember { mutableStateOf(0) }
|
||||||
val currentRoute = route.value
|
val currentRoute = route.value
|
||||||
|
val canManageProperty: (String) -> Boolean = { propertyId ->
|
||||||
|
state.isSuperAdmin || (state.propertyRoles[propertyId]?.contains("ADMIN") == true)
|
||||||
|
}
|
||||||
|
|
||||||
when (currentRoute) {
|
when (currentRoute) {
|
||||||
AppRoute.Home -> HomeScreen(
|
AppRoute.Home -> HomeScreen(
|
||||||
@@ -89,7 +98,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
route.value = AppRoute.AddRoom(currentRoute.propertyId)
|
route.value = AppRoute.AddRoom(currentRoute.propertyId)
|
||||||
},
|
},
|
||||||
onViewRoomTypes = { route.value = AppRoute.RoomTypes(currentRoute.propertyId) },
|
onViewRoomTypes = { route.value = AppRoute.RoomTypes(currentRoute.propertyId) },
|
||||||
canManageRooms = state.isSuperAdmin,
|
canManageRooms = canManageProperty(currentRoute.propertyId),
|
||||||
onEditRoom = {
|
onEditRoom = {
|
||||||
selectedRoom.value = it
|
selectedRoom.value = it
|
||||||
roomFormKey.value++
|
roomFormKey.value++
|
||||||
@@ -99,13 +108,47 @@ class MainActivity : ComponentActivity() {
|
|||||||
is AppRoute.RoomTypes -> RoomTypesScreen(
|
is AppRoute.RoomTypes -> RoomTypesScreen(
|
||||||
propertyId = currentRoute.propertyId,
|
propertyId = currentRoute.propertyId,
|
||||||
onBack = { route.value = AppRoute.Rooms(currentRoute.propertyId) },
|
onBack = { route.value = AppRoute.Rooms(currentRoute.propertyId) },
|
||||||
onAdd = { route.value = AppRoute.AddRoomType(currentRoute.propertyId) }
|
onAdd = { route.value = AppRoute.AddRoomType(currentRoute.propertyId) },
|
||||||
|
onAmenities = { route.value = AppRoute.Amenities(currentRoute.propertyId) },
|
||||||
|
canManageRoomTypes = canManageProperty(currentRoute.propertyId),
|
||||||
|
onEdit = {
|
||||||
|
selectedRoomType.value = it
|
||||||
|
route.value = AppRoute.EditRoomType(currentRoute.propertyId, it.id ?: "")
|
||||||
|
}
|
||||||
)
|
)
|
||||||
is AppRoute.AddRoomType -> AddRoomTypeScreen(
|
is AppRoute.AddRoomType -> AddRoomTypeScreen(
|
||||||
propertyId = currentRoute.propertyId,
|
propertyId = currentRoute.propertyId,
|
||||||
onBack = { route.value = AppRoute.RoomTypes(currentRoute.propertyId) },
|
onBack = { route.value = AppRoute.RoomTypes(currentRoute.propertyId) },
|
||||||
onSave = { route.value = AppRoute.RoomTypes(currentRoute.propertyId) }
|
onSave = { route.value = AppRoute.RoomTypes(currentRoute.propertyId) }
|
||||||
)
|
)
|
||||||
|
is AppRoute.EditRoomType -> EditRoomTypeScreen(
|
||||||
|
propertyId = currentRoute.propertyId,
|
||||||
|
roomType = selectedRoomType.value
|
||||||
|
?: com.android.trisolarispms.data.api.model.RoomTypeDto(id = currentRoute.roomTypeId, code = "", name = ""),
|
||||||
|
onBack = { route.value = AppRoute.RoomTypes(currentRoute.propertyId) },
|
||||||
|
onSave = { route.value = AppRoute.RoomTypes(currentRoute.propertyId) }
|
||||||
|
)
|
||||||
|
is AppRoute.Amenities -> AmenitiesScreen(
|
||||||
|
propertyId = currentRoute.propertyId,
|
||||||
|
onBack = { route.value = AppRoute.RoomTypes(currentRoute.propertyId) },
|
||||||
|
onAdd = { route.value = AppRoute.AddAmenity(currentRoute.propertyId) },
|
||||||
|
onEdit = {
|
||||||
|
selectedAmenity.value = it
|
||||||
|
route.value = AppRoute.EditAmenity(currentRoute.propertyId, it.id ?: "")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
is AppRoute.AddAmenity -> AddAmenityScreen(
|
||||||
|
propertyId = currentRoute.propertyId,
|
||||||
|
onBack = { route.value = AppRoute.Amenities(currentRoute.propertyId) },
|
||||||
|
onSave = { route.value = AppRoute.Amenities(currentRoute.propertyId) }
|
||||||
|
)
|
||||||
|
is AppRoute.EditAmenity -> EditAmenityScreen(
|
||||||
|
propertyId = currentRoute.propertyId,
|
||||||
|
amenity = selectedAmenity.value
|
||||||
|
?: com.android.trisolarispms.data.api.model.AmenityDto(id = currentRoute.amenityId, name = ""),
|
||||||
|
onBack = { route.value = AppRoute.Amenities(currentRoute.propertyId) },
|
||||||
|
onSave = { route.value = AppRoute.Amenities(currentRoute.propertyId) }
|
||||||
|
)
|
||||||
is AppRoute.AddRoom -> RoomFormScreen(
|
is AppRoute.AddRoom -> RoomFormScreen(
|
||||||
title = "Add Room",
|
title = "Add Room",
|
||||||
propertyId = currentRoute.propertyId,
|
propertyId = currentRoute.propertyId,
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.android.trisolarispms.data.api
|
||||||
|
|
||||||
|
import com.android.trisolarispms.data.api.model.AmenityCreateRequest
|
||||||
|
import com.android.trisolarispms.data.api.model.AmenityDto
|
||||||
|
import com.android.trisolarispms.data.api.model.AmenityUpdateRequest
|
||||||
|
import retrofit2.Response
|
||||||
|
import retrofit2.http.Body
|
||||||
|
import retrofit2.http.DELETE
|
||||||
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.POST
|
||||||
|
import retrofit2.http.PUT
|
||||||
|
import retrofit2.http.Path
|
||||||
|
|
||||||
|
interface AmenityApi {
|
||||||
|
@GET("properties/{propertyId}/amenities")
|
||||||
|
suspend fun listAmenities(@Path("propertyId") propertyId: String): Response<List<AmenityDto>>
|
||||||
|
|
||||||
|
@POST("properties/{propertyId}/amenities")
|
||||||
|
suspend fun createAmenity(
|
||||||
|
@Path("propertyId") propertyId: String,
|
||||||
|
@Body body: AmenityCreateRequest
|
||||||
|
): Response<AmenityDto>
|
||||||
|
|
||||||
|
@PUT("properties/{propertyId}/amenities/{amenityId}")
|
||||||
|
suspend fun updateAmenity(
|
||||||
|
@Path("propertyId") propertyId: String,
|
||||||
|
@Path("amenityId") amenityId: String,
|
||||||
|
@Body body: AmenityUpdateRequest
|
||||||
|
): Response<AmenityDto>
|
||||||
|
|
||||||
|
@DELETE("properties/{propertyId}/amenities/{amenityId}")
|
||||||
|
suspend fun deleteAmenity(
|
||||||
|
@Path("propertyId") propertyId: String,
|
||||||
|
@Path("amenityId") amenityId: String
|
||||||
|
): Response<Unit>
|
||||||
|
}
|
||||||
@@ -12,4 +12,5 @@ interface ApiService :
|
|||||||
GuestApi,
|
GuestApi,
|
||||||
GuestDocumentApi,
|
GuestDocumentApi,
|
||||||
TransportApi,
|
TransportApi,
|
||||||
InboundEmailApi
|
InboundEmailApi,
|
||||||
|
AmenityApi
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.android.trisolarispms.data.api.model
|
||||||
|
|
||||||
|
data class AmenityDto(
|
||||||
|
val id: String? = null,
|
||||||
|
val name: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class AmenityCreateRequest(
|
||||||
|
val name: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class AmenityUpdateRequest(
|
||||||
|
val name: String
|
||||||
|
)
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
package com.android.trisolarispms.data.api.model
|
package com.android.trisolarispms.data.api.model
|
||||||
|
|
||||||
|
import com.android.trisolarispms.data.api.model.AmenityDto
|
||||||
|
|
||||||
data class RoomTypeCreateRequest(
|
data class RoomTypeCreateRequest(
|
||||||
val code: String,
|
val code: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
val baseOccupancy: Int? = null,
|
val baseOccupancy: Int? = null,
|
||||||
val maxOccupancy: Int? = null,
|
val maxOccupancy: Int? = null,
|
||||||
|
val sqFeet: Int? = null,
|
||||||
|
val bathroomSqFeet: Int? = null,
|
||||||
|
val amenityIds: List<String>? = null,
|
||||||
val otaAliases: List<String>? = null
|
val otaAliases: List<String>? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -13,6 +18,9 @@ data class RoomTypeUpdateRequest(
|
|||||||
val name: String? = null,
|
val name: String? = null,
|
||||||
val baseOccupancy: Int? = null,
|
val baseOccupancy: Int? = null,
|
||||||
val maxOccupancy: Int? = null,
|
val maxOccupancy: Int? = null,
|
||||||
|
val sqFeet: Int? = null,
|
||||||
|
val bathroomSqFeet: Int? = null,
|
||||||
|
val amenityIds: List<String>? = null,
|
||||||
val otaAliases: List<String>? = null
|
val otaAliases: List<String>? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -23,5 +31,8 @@ data class RoomTypeDto(
|
|||||||
val name: String? = null,
|
val name: String? = null,
|
||||||
val baseOccupancy: Int? = null,
|
val baseOccupancy: Int? = null,
|
||||||
val maxOccupancy: Int? = null,
|
val maxOccupancy: Int? = null,
|
||||||
|
val sqFeet: Int? = null,
|
||||||
|
val bathroomSqFeet: Int? = null,
|
||||||
|
val amenities: List<AmenityDto>? = null,
|
||||||
val otaAliases: List<String>? = null
|
val otaAliases: List<String>? = null
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,4 +9,8 @@ sealed interface AppRoute {
|
|||||||
data class EditRoom(val propertyId: String, val roomId: String) : AppRoute
|
data class EditRoom(val propertyId: String, val roomId: String) : AppRoute
|
||||||
data class RoomTypes(val propertyId: String) : AppRoute
|
data class RoomTypes(val propertyId: String) : AppRoute
|
||||||
data class AddRoomType(val propertyId: String) : AppRoute
|
data class AddRoomType(val propertyId: String) : AppRoute
|
||||||
|
data class EditRoomType(val propertyId: String, val roomTypeId: String) : AppRoute
|
||||||
|
data class Amenities(val propertyId: String) : AppRoute
|
||||||
|
data class AddAmenity(val propertyId: String) : AppRoute
|
||||||
|
data class EditAmenity(val propertyId: String, val amenityId: String) : AppRoute
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,5 +15,6 @@ data class AuthUiState(
|
|||||||
val userName: String? = null,
|
val userName: String? = null,
|
||||||
val nameInput: String = "",
|
val nameInput: String = "",
|
||||||
val needsName: Boolean = false,
|
val needsName: Boolean = false,
|
||||||
val unauthorized: Boolean = false
|
val unauthorized: Boolean = false,
|
||||||
|
val propertyRoles: Map<String, List<String>> = emptyMap()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -161,6 +161,13 @@ class AuthViewModel(
|
|||||||
val body = response.body()
|
val body = response.body()
|
||||||
val userName = body?.user?.name
|
val userName = body?.user?.name
|
||||||
val isSuperAdmin = body?.user?.superAdmin == true
|
val isSuperAdmin = body?.user?.superAdmin == true
|
||||||
|
val propertyRoles = body?.properties
|
||||||
|
.orEmpty()
|
||||||
|
.mapNotNull { entry ->
|
||||||
|
val id = entry.propertyId
|
||||||
|
id?.let { it to entry.roles.orEmpty() }
|
||||||
|
}
|
||||||
|
.toMap()
|
||||||
when {
|
when {
|
||||||
response.isSuccessful && (body?.status == "OK" || body?.status == "SUPER_ADMIN") -> {
|
response.isSuccessful && (body?.status == "OK" || body?.status == "SUPER_ADMIN") -> {
|
||||||
_state.update {
|
_state.update {
|
||||||
@@ -174,6 +181,7 @@ class AuthViewModel(
|
|||||||
isSuperAdmin = isSuperAdmin,
|
isSuperAdmin = isSuperAdmin,
|
||||||
noProperties = body?.status == "NO_PROPERTIES",
|
noProperties = body?.status == "NO_PROPERTIES",
|
||||||
unauthorized = false,
|
unauthorized = false,
|
||||||
|
propertyRoles = propertyRoles,
|
||||||
error = null
|
error = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -190,6 +198,7 @@ class AuthViewModel(
|
|||||||
isSuperAdmin = isSuperAdmin,
|
isSuperAdmin = isSuperAdmin,
|
||||||
noProperties = true,
|
noProperties = true,
|
||||||
unauthorized = false,
|
unauthorized = false,
|
||||||
|
propertyRoles = propertyRoles,
|
||||||
error = null
|
error = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -202,6 +211,7 @@ class AuthViewModel(
|
|||||||
isSuperAdmin = false,
|
isSuperAdmin = false,
|
||||||
noProperties = false,
|
noProperties = false,
|
||||||
unauthorized = true,
|
unauthorized = true,
|
||||||
|
propertyRoles = emptyMap(),
|
||||||
error = "Not authorized. Contact admin."
|
error = "Not authorized. Contact admin."
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -214,6 +224,7 @@ class AuthViewModel(
|
|||||||
isSuperAdmin = false,
|
isSuperAdmin = false,
|
||||||
noProperties = false,
|
noProperties = false,
|
||||||
unauthorized = false,
|
unauthorized = false,
|
||||||
|
propertyRoles = emptyMap(),
|
||||||
error = "API verify failed: ${response.code()}"
|
error = "API verify failed: ${response.code()}"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package com.android.trisolarispms.ui.roomtype
|
||||||
|
|
||||||
|
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.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
fun AddAmenityScreen(
|
||||||
|
propertyId: String,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
onSave: () -> Unit,
|
||||||
|
viewModel: AmenityFormViewModel = viewModel()
|
||||||
|
) {
|
||||||
|
val state by viewModel.state.collectAsState()
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text("Add Amenity") },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onBack) {
|
||||||
|
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
IconButton(onClick = { viewModel.submitCreate(propertyId, 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,15 +2,18 @@ package com.android.trisolarispms.ui.roomtype
|
|||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.ArrowBack
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.Done
|
import androidx.compose.material.icons.filled.Done
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
@@ -35,9 +38,15 @@ fun AddRoomTypeScreen(
|
|||||||
propertyId: String,
|
propertyId: String,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onSave: () -> Unit,
|
onSave: () -> Unit,
|
||||||
viewModel: RoomTypeFormViewModel = viewModel()
|
viewModel: RoomTypeFormViewModel = viewModel(),
|
||||||
|
amenityViewModel: AmenityListViewModel = viewModel()
|
||||||
) {
|
) {
|
||||||
val state by viewModel.state.collectAsState()
|
val state by viewModel.state.collectAsState()
|
||||||
|
val amenityState by amenityViewModel.state.collectAsState()
|
||||||
|
|
||||||
|
LaunchedEffect(propertyId) {
|
||||||
|
amenityViewModel.load(propertyId)
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
@@ -98,6 +107,45 @@ fun AddRoomTypeScreen(
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state.sqFeet,
|
||||||
|
onValueChange = viewModel::onSqFeetChange,
|
||||||
|
label = { Text("Sq Ft") },
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state.bathroomSqFeet,
|
||||||
|
onValueChange = viewModel::onBathroomSqFeetChange,
|
||||||
|
label = { Text("Bathroom Sq Ft") },
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
Text(text = "Amenities", style = MaterialTheme.typography.titleSmall)
|
||||||
|
if (amenityState.items.isEmpty()) {
|
||||||
|
Text(text = "No amenities available", style = MaterialTheme.typography.bodySmall)
|
||||||
|
} else {
|
||||||
|
amenityState.items.forEach { amenity ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.padding(vertical = 4.dp)
|
||||||
|
) {
|
||||||
|
Checkbox(
|
||||||
|
checked = state.amenityIds.contains(amenity.id),
|
||||||
|
onCheckedChange = { amenity.id?.let { viewModel.onAmenityToggle(it) } }
|
||||||
|
)
|
||||||
|
Text(text = amenity.name ?: "", modifier = Modifier.padding(start = 8.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = state.otaAliases,
|
value = state.otaAliases,
|
||||||
onValueChange = viewModel::onAliasesChange,
|
onValueChange = viewModel::onAliasesChange,
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package com.android.trisolarispms.ui.roomtype
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
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.Add
|
||||||
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
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.AmenityDto
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
fun AmenitiesScreen(
|
||||||
|
propertyId: String,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
onAdd: () -> Unit,
|
||||||
|
onEdit: (AmenityDto) -> Unit,
|
||||||
|
viewModel: AmenityListViewModel = viewModel()
|
||||||
|
) {
|
||||||
|
val state by viewModel.state.collectAsState()
|
||||||
|
|
||||||
|
LaunchedEffect(propertyId) {
|
||||||
|
viewModel.load(propertyId)
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text("Amenities") },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onBack) {
|
||||||
|
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
IconButton(onClick = onAdd) {
|
||||||
|
Icon(Icons.Default.Add, contentDescription = "Add Amenity")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { padding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(padding)
|
||||||
|
.padding(24.dp),
|
||||||
|
verticalArrangement = Arrangement.Top
|
||||||
|
) {
|
||||||
|
if (state.isLoading) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
state.error?.let {
|
||||||
|
Text(text = it, color = MaterialTheme.colorScheme.error)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
if (!state.isLoading && state.error == null) {
|
||||||
|
if (state.items.isEmpty()) {
|
||||||
|
Text(text = "No amenities")
|
||||||
|
} else {
|
||||||
|
state.items.forEach { item ->
|
||||||
|
Text(
|
||||||
|
text = item.name ?: "",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(enabled = item.id != null) { onEdit(item) }
|
||||||
|
.padding(vertical = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.android.trisolarispms.ui.roomtype
|
||||||
|
|
||||||
|
data class AmenityFormState(
|
||||||
|
val name: String = "",
|
||||||
|
val isLoading: Boolean = false,
|
||||||
|
val error: String? = null,
|
||||||
|
val success: Boolean = false
|
||||||
|
)
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package com.android.trisolarispms.ui.roomtype
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.android.trisolarispms.data.api.ApiClient
|
||||||
|
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
|
||||||
|
|
||||||
|
class AmenityFormViewModel : ViewModel() {
|
||||||
|
private val _state = MutableStateFlow(AmenityFormState())
|
||||||
|
val state: StateFlow<AmenityFormState> = _state
|
||||||
|
|
||||||
|
fun setAmenityName(name: String) {
|
||||||
|
_state.update { it.copy(name = name, error = null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onNameChange(value: String) {
|
||||||
|
_state.update { it.copy(name = value, error = null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun submitCreate(propertyId: String, 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.createAmenity(propertyId, AmenityCreateRequest(name))
|
||||||
|
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, amenityId: String, 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.updateAmenity(propertyId, amenityId, AmenityUpdateRequest(name))
|
||||||
|
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") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.android.trisolarispms.ui.roomtype
|
||||||
|
|
||||||
|
import com.android.trisolarispms.data.api.model.AmenityDto
|
||||||
|
|
||||||
|
data class AmenityListState(
|
||||||
|
val isLoading: Boolean = false,
|
||||||
|
val error: String? = null,
|
||||||
|
val items: List<AmenityDto> = emptyList()
|
||||||
|
)
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.android.trisolarispms.ui.roomtype
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.android.trisolarispms.data.api.ApiClient
|
||||||
|
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(propertyId: String) {
|
||||||
|
if (propertyId.isBlank()) return
|
||||||
|
viewModelScope.launch {
|
||||||
|
_state.update { it.copy(isLoading = true, error = null) }
|
||||||
|
try {
|
||||||
|
val api = ApiClient.create()
|
||||||
|
val response = api.listAmenities(propertyId)
|
||||||
|
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()}") }
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_state.update { it.copy(isLoading = false, error = e.localizedMessage ?: "Load failed") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package com.android.trisolarispms.ui.roomtype
|
||||||
|
|
||||||
|
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.AmenityDto
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
fun EditAmenityScreen(
|
||||||
|
propertyId: String,
|
||||||
|
amenity: AmenityDto,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
onSave: () -> Unit,
|
||||||
|
viewModel: AmenityFormViewModel = viewModel()
|
||||||
|
) {
|
||||||
|
val state by viewModel.state.collectAsState()
|
||||||
|
|
||||||
|
LaunchedEffect(amenity.id) {
|
||||||
|
viewModel.setAmenityName(amenity.name.orEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text("Edit Amenity") },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onBack) {
|
||||||
|
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
IconButton(onClick = {
|
||||||
|
val id = amenity.id.orEmpty()
|
||||||
|
viewModel.submitUpdate(propertyId, id, 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
package com.android.trisolarispms.ui.roomtype
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
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.foundation.layout.wrapContentHeight
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.Done
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
|
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.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.android.trisolarispms.data.api.model.RoomTypeDto
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
fun EditRoomTypeScreen(
|
||||||
|
propertyId: String,
|
||||||
|
roomType: RoomTypeDto,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
onSave: () -> Unit,
|
||||||
|
viewModel: RoomTypeFormViewModel = viewModel(),
|
||||||
|
amenityViewModel: AmenityListViewModel = viewModel()
|
||||||
|
) {
|
||||||
|
val state by viewModel.state.collectAsState()
|
||||||
|
val amenityState by amenityViewModel.state.collectAsState()
|
||||||
|
|
||||||
|
LaunchedEffect(roomType.id) {
|
||||||
|
viewModel.setRoomType(roomType)
|
||||||
|
}
|
||||||
|
LaunchedEffect(propertyId) {
|
||||||
|
amenityViewModel.load(propertyId)
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text("Modify Room Type") },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onBack) {
|
||||||
|
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
IconButton(onClick = { viewModel.submitUpdate(propertyId, roomType.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.code,
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true,
|
||||||
|
label = { Text("Code") },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state.name,
|
||||||
|
onValueChange = viewModel::onNameChange,
|
||||||
|
label = { Text("Name") },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state.baseOccupancy,
|
||||||
|
onValueChange = viewModel::onBaseOccupancyChange,
|
||||||
|
label = { Text("Base Occupancy") },
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state.maxOccupancy,
|
||||||
|
onValueChange = viewModel::onMaxOccupancyChange,
|
||||||
|
label = { Text("Max Occupancy") },
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state.sqFeet,
|
||||||
|
onValueChange = viewModel::onSqFeetChange,
|
||||||
|
label = { Text("Sq Ft") },
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state.bathroomSqFeet,
|
||||||
|
onValueChange = viewModel::onBathroomSqFeetChange,
|
||||||
|
label = { Text("Bathroom Sq Ft") },
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
Text(text = "Amenities", style = MaterialTheme.typography.titleSmall)
|
||||||
|
if (amenityState.items.isEmpty()) {
|
||||||
|
Text(text = "No amenities available", style = MaterialTheme.typography.bodySmall)
|
||||||
|
} else {
|
||||||
|
amenityState.items.forEach { amenity ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.wrapContentHeight()
|
||||||
|
.padding(vertical = 4.dp)
|
||||||
|
) {
|
||||||
|
Checkbox(
|
||||||
|
checked = state.amenityIds.contains(amenity.id),
|
||||||
|
onCheckedChange = { amenity.id?.let { viewModel.onAmenityToggle(it) } }
|
||||||
|
)
|
||||||
|
Text(text = amenity.name ?: "", modifier = Modifier.padding(start = 8.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state.otaAliases,
|
||||||
|
onValueChange = viewModel::onAliasesChange,
|
||||||
|
label = { Text("OTA Aliases (comma separated)") },
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
state.error?.let {
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
Text(text = it, color = MaterialTheme.colorScheme.error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,9 @@ data class RoomTypeFormState(
|
|||||||
val name: String = "",
|
val name: String = "",
|
||||||
val baseOccupancy: String = "",
|
val baseOccupancy: String = "",
|
||||||
val maxOccupancy: String = "",
|
val maxOccupancy: String = "",
|
||||||
|
val sqFeet: String = "",
|
||||||
|
val bathroomSqFeet: String = "",
|
||||||
|
val amenityIds: Set<String> = emptySet(),
|
||||||
val otaAliases: String = "",
|
val otaAliases: String = "",
|
||||||
val isLoading: Boolean = false,
|
val isLoading: Boolean = false,
|
||||||
val error: String? = null,
|
val error: String? = null,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.android.trisolarispms.data.api.ApiClient
|
import com.android.trisolarispms.data.api.ApiClient
|
||||||
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 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
|
||||||
@@ -13,10 +14,33 @@ class RoomTypeFormViewModel : ViewModel() {
|
|||||||
private val _state = MutableStateFlow(RoomTypeFormState())
|
private val _state = MutableStateFlow(RoomTypeFormState())
|
||||||
val state: StateFlow<RoomTypeFormState> = _state
|
val state: StateFlow<RoomTypeFormState> = _state
|
||||||
|
|
||||||
|
fun setRoomType(type: com.android.trisolarispms.data.api.model.RoomTypeDto) {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
code = type.code ?: "",
|
||||||
|
name = type.name ?: "",
|
||||||
|
baseOccupancy = type.baseOccupancy?.toString() ?: "",
|
||||||
|
maxOccupancy = type.maxOccupancy?.toString() ?: "",
|
||||||
|
sqFeet = type.sqFeet?.toString() ?: "",
|
||||||
|
bathroomSqFeet = type.bathroomSqFeet?.toString() ?: "",
|
||||||
|
amenityIds = type.amenities?.mapNotNull { it.id }?.toSet() ?: emptySet(),
|
||||||
|
otaAliases = type.otaAliases?.joinToString(",") ?: "",
|
||||||
|
error = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun onCodeChange(value: String) = _state.update { it.copy(code = value.trim().uppercase(), error = null) }
|
fun onCodeChange(value: String) = _state.update { it.copy(code = value.trim().uppercase(), error = null) }
|
||||||
fun onNameChange(value: String) = _state.update { it.copy(name = value, error = null) }
|
fun onNameChange(value: String) = _state.update { it.copy(name = value, error = null) }
|
||||||
fun onBaseOccupancyChange(value: String) = _state.update { it.copy(baseOccupancy = value, error = null) }
|
fun onBaseOccupancyChange(value: String) = _state.update { it.copy(baseOccupancy = value, error = null) }
|
||||||
fun onMaxOccupancyChange(value: String) = _state.update { it.copy(maxOccupancy = value, error = null) }
|
fun onMaxOccupancyChange(value: String) = _state.update { it.copy(maxOccupancy = value, error = null) }
|
||||||
|
fun onSqFeetChange(value: String) = _state.update { it.copy(sqFeet = value, error = null) }
|
||||||
|
fun onBathroomSqFeetChange(value: String) = _state.update { it.copy(bathroomSqFeet = value, error = null) }
|
||||||
|
fun onAmenityToggle(id: String) = _state.update { current ->
|
||||||
|
val next = current.amenityIds.toMutableSet()
|
||||||
|
if (next.contains(id)) next.remove(id) else next.add(id)
|
||||||
|
current.copy(amenityIds = next, error = null)
|
||||||
|
}
|
||||||
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) {
|
||||||
@@ -35,6 +59,9 @@ class RoomTypeFormViewModel : ViewModel() {
|
|||||||
name = name,
|
name = name,
|
||||||
baseOccupancy = state.value.baseOccupancy.toIntOrNull(),
|
baseOccupancy = state.value.baseOccupancy.toIntOrNull(),
|
||||||
maxOccupancy = state.value.maxOccupancy.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 }
|
otaAliases = state.value.otaAliases.split(',').map { it.trim() }.filter { it.isNotBlank() }.ifEmpty { null }
|
||||||
)
|
)
|
||||||
val response = api.createRoomType(propertyId, body)
|
val response = api.createRoomType(propertyId, body)
|
||||||
@@ -49,4 +76,38 @@ class RoomTypeFormViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.android.trisolarispms.ui.roomtype
|
package com.android.trisolarispms.ui.roomtype
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@@ -26,6 +27,7 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import androidx.compose.material.icons.filled.Settings
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@@ -33,6 +35,9 @@ fun RoomTypesScreen(
|
|||||||
propertyId: String,
|
propertyId: String,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onAdd: () -> Unit,
|
onAdd: () -> Unit,
|
||||||
|
onAmenities: () -> Unit,
|
||||||
|
canManageRoomTypes: Boolean,
|
||||||
|
onEdit: (com.android.trisolarispms.data.api.model.RoomTypeDto) -> Unit,
|
||||||
viewModel: RoomTypeListViewModel = viewModel()
|
viewModel: RoomTypeListViewModel = viewModel()
|
||||||
) {
|
) {
|
||||||
val state by viewModel.state.collectAsState()
|
val state by viewModel.state.collectAsState()
|
||||||
@@ -51,9 +56,14 @@ fun RoomTypesScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
|
if (canManageRoomTypes) {
|
||||||
|
IconButton(onClick = onAmenities) {
|
||||||
|
Icon(Icons.Default.Settings, contentDescription = "Amenities")
|
||||||
|
}
|
||||||
IconButton(onClick = onAdd) {
|
IconButton(onClick = onAdd) {
|
||||||
Icon(Icons.Default.Add, contentDescription = "Add Room Type")
|
Icon(Icons.Default.Add, contentDescription = "Add Room Type")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
colors = TopAppBarDefaults.topAppBarColors()
|
colors = TopAppBarDefaults.topAppBarColors()
|
||||||
)
|
)
|
||||||
@@ -79,8 +89,16 @@ fun RoomTypesScreen(
|
|||||||
Text(text = "No room types")
|
Text(text = "No room types")
|
||||||
} else {
|
} else {
|
||||||
state.items.forEach { item ->
|
state.items.forEach { item ->
|
||||||
Text(text = "${item.code} • ${item.name}", style = MaterialTheme.typography.titleMedium)
|
Text(
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
text = "${item.code} • ${item.name}",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(enabled = item.id != null) {
|
||||||
|
onEdit(item)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user