diff --git a/app/src/main/java/com/android/trisolarispms/ui/booking/BookingCreateScreen.kt b/app/src/main/java/com/android/trisolarispms/ui/booking/BookingCreateScreen.kt index ecd5184..fc617cb 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/booking/BookingCreateScreen.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/booking/BookingCreateScreen.kt @@ -14,10 +14,8 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.CalendarMonth import androidx.compose.material.icons.filled.Schedule -import androidx.compose.material.icons.filled.Done import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExposedDropdownMenuBox @@ -27,11 +25,8 @@ 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.Switch 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 @@ -46,6 +41,7 @@ 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.BookingBillingMode +import com.android.trisolarispms.ui.common.SaveTopBarScaffold import java.time.LocalDate import java.time.OffsetDateTime import java.time.format.DateTimeFormatter @@ -94,23 +90,10 @@ fun BookingCreateScreen( viewModel.onExpectedCheckOutAtChange(formatBookingIso(defaultCheckoutDate, checkOutTime.value)) } - Scaffold( - topBar = { - TopAppBar( - title = { Text("Create Booking") }, - navigationIcon = { - IconButton(onClick = onBack) { - Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = "Back") - } - }, - actions = { - IconButton(onClick = { viewModel.submit(propertyId, onCreated) }) { - Icon(Icons.Default.Done, contentDescription = "Save") - } - }, - colors = TopAppBarDefaults.topAppBarColors() - ) - } + SaveTopBarScaffold( + title = "Create Booking", + onBack = onBack, + onSave = { viewModel.submit(propertyId, onCreated) } ) { padding -> Column( modifier = Modifier diff --git a/app/src/main/java/com/android/trisolarispms/ui/booking/BookingExpectedDatesScreen.kt b/app/src/main/java/com/android/trisolarispms/ui/booking/BookingExpectedDatesScreen.kt index a598adf..42824b4 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/booking/BookingExpectedDatesScreen.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/booking/BookingExpectedDatesScreen.kt @@ -3,24 +3,16 @@ package com.android.trisolarispms.ui.booking 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.CalendarMonth -import androidx.compose.material.icons.filled.Done 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.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.mutableStateOf @@ -30,6 +22,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.android.trisolarispms.data.api.core.ApiClient import com.android.trisolarispms.data.api.model.BookingExpectedDatesRequest +import com.android.trisolarispms.ui.common.PaddedScreenColumn +import com.android.trisolarispms.ui.common.SaveTopBarScaffold import kotlinx.coroutines.launch import java.time.LocalDate import java.time.OffsetDateTime @@ -37,7 +31,6 @@ import java.time.ZoneId import java.time.format.DateTimeFormatter @Composable -@OptIn(ExperimentalMaterial3Api::class) fun BookingExpectedDatesScreen( propertyId: String, bookingId: String, @@ -84,65 +77,44 @@ fun BookingExpectedDatesScreen( } } - Scaffold( - topBar = { - TopAppBar( - title = { Text("Update Expected Dates") }, - navigationIcon = { - IconButton(onClick = onBack) { - Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = "Back") + SaveTopBarScaffold( + title = "Update Expected Dates", + onBack = onBack, + saveEnabled = !isLoading.value, + onSave = { + isLoading.value = true + error.value = null + val inAt = if (editableCheckIn) { + checkInDate.value?.let { formatBookingIso(it, checkInTime.value) } + } else { + null + } + val outAt = checkOutDate.value?.let { formatBookingIso(it, checkOutTime.value) } + scope.launch { + try { + val api = ApiClient.create() + val response = api.updateExpectedDates( + propertyId = propertyId, + bookingId = bookingId, + body = BookingExpectedDatesRequest( + expectedCheckInAt = inAt, + expectedCheckOutAt = outAt + ) + ) + if (response.isSuccessful) { + onDone() + } else { + error.value = "Update failed: ${response.code()}" } - }, - actions = { - IconButton( - onClick = { - isLoading.value = true - error.value = null - val inAt = if (editableCheckIn) { - checkInDate.value?.let { formatBookingIso(it, checkInTime.value) } - } else { - null - } - val outAt = checkOutDate.value?.let { formatBookingIso(it, checkOutTime.value) } - scope.launch { - try { - val api = ApiClient.create() - val response = api.updateExpectedDates( - propertyId = propertyId, - bookingId = bookingId, - body = BookingExpectedDatesRequest( - expectedCheckInAt = inAt, - expectedCheckOutAt = outAt - ) - ) - if (response.isSuccessful) { - onDone() - } else { - error.value = "Update failed: ${response.code()}" - } - } catch (e: Exception) { - error.value = e.localizedMessage ?: "Update failed" - } finally { - isLoading.value = false - } - } - }, - enabled = !isLoading.value - ) { - Icon(Icons.Default.Done, contentDescription = "Save") - } - }, - colors = TopAppBarDefaults.topAppBarColors() - ) + } catch (e: Exception) { + error.value = e.localizedMessage ?: "Update failed" + } finally { + isLoading.value = false + } + } } ) { padding -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(padding) - .padding(24.dp), - verticalArrangement = Arrangement.Top - ) { + PaddedScreenColumn(padding = padding) { if (editableCheckIn) { OutlinedTextField( value = checkInDate.value?.let { diff --git a/app/src/main/java/com/android/trisolarispms/ui/guest/GuestInfoScreen.kt b/app/src/main/java/com/android/trisolarispms/ui/guest/GuestInfoScreen.kt index 38b7f0d..a1fa62b 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/guest/GuestInfoScreen.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/guest/GuestInfoScreen.kt @@ -3,23 +3,12 @@ package com.android.trisolarispms.ui.guest 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.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.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 @@ -27,9 +16,10 @@ 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.ui.common.PaddedScreenColumn +import com.android.trisolarispms.ui.common.SaveTopBarScaffold @Composable -@OptIn(ExperimentalMaterial3Api::class) fun GuestInfoScreen( propertyId: String, guestId: String, @@ -47,31 +37,12 @@ fun GuestInfoScreen( viewModel.loadGuest(propertyId, guestId, initialPhone) } - Scaffold( - topBar = { - TopAppBar( - title = { Text("Guest Info") }, - navigationIcon = { - IconButton(onClick = onBack) { - Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = "Back") - } - }, - actions = { - IconButton(onClick = { viewModel.submit(propertyId, guestId, onSave) }) { - Icon(Icons.Default.Done, contentDescription = "Save") - } - }, - colors = TopAppBarDefaults.topAppBarColors() - ) - } + SaveTopBarScaffold( + title = "Guest Info", + onBack = onBack, + onSave = { viewModel.submit(propertyId, guestId, onSave) } ) { padding -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(padding) - .padding(24.dp), - verticalArrangement = Arrangement.Top - ) { + PaddedScreenColumn(padding = padding) { OutlinedTextField( value = state.phoneE164, onValueChange = viewModel::onPhoneChange, diff --git a/app/src/main/java/com/android/trisolarispms/ui/guest/GuestSignatureScreen.kt b/app/src/main/java/com/android/trisolarispms/ui/guest/GuestSignatureScreen.kt index e877323..0baf0b3 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/guest/GuestSignatureScreen.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/guest/GuestSignatureScreen.kt @@ -8,24 +8,17 @@ import androidx.compose.foundation.gestures.drag 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.foundation.layout.size 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.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.OutlinedButton -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 @@ -48,9 +41,10 @@ import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import java.util.Locale +import com.android.trisolarispms.ui.common.BackTopBarScaffold +import com.android.trisolarispms.ui.common.PaddedScreenColumn @Composable -@OptIn(ExperimentalMaterial3Api::class) fun GuestSignatureScreen( propertyId: String, guestId: String, @@ -66,43 +60,28 @@ fun GuestSignatureScreen( viewModel.reset() } - Scaffold( - topBar = { - TopAppBar( - title = { Text("Guest Signature") }, - navigationIcon = { - IconButton(onClick = onBack) { - Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = "Back") + BackTopBarScaffold( + title = "Guest Signature", + onBack = onBack, + actions = { + IconButton( + onClick = { + val svg = buildSignatureSvg(strokes, canvasSize.value) + if (!svg.isNullOrBlank()) { + viewModel.uploadSignature(propertyId, guestId, svg, onDone) } }, - actions = { - IconButton( - onClick = { - val svg = buildSignatureSvg(strokes, canvasSize.value) - if (!svg.isNullOrBlank()) { - viewModel.uploadSignature(propertyId, guestId, svg, onDone) - } - }, - enabled = strokes.isNotEmpty() && !state.isLoading - ) { - if (state.isLoading) { - CircularProgressIndicator(modifier = Modifier.size(18.dp), strokeWidth = 2.dp) - } else { - Icon(Icons.Default.Done, contentDescription = "Upload") - } - } - }, - colors = TopAppBarDefaults.topAppBarColors() - ) + enabled = strokes.isNotEmpty() && !state.isLoading + ) { + if (state.isLoading) { + CircularProgressIndicator(modifier = Modifier.size(18.dp), strokeWidth = 2.dp) + } else { + Icon(Icons.Default.Done, contentDescription = "Upload") + } + } } ) { padding -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(padding) - .padding(24.dp), - verticalArrangement = Arrangement.Top - ) { + PaddedScreenColumn(padding = padding) { Text( text = "Please draw the guest signature below.", style = MaterialTheme.typography.bodyMedium diff --git a/app/src/main/java/com/android/trisolarispms/ui/home/HomeScreen.kt b/app/src/main/java/com/android/trisolarispms/ui/home/HomeScreen.kt index 5939278..eef2f10 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/home/HomeScreen.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/home/HomeScreen.kt @@ -4,7 +4,6 @@ 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 @@ -14,14 +13,10 @@ import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem -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 @@ -36,9 +31,10 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.android.trisolarispms.core.auth.Role import com.android.trisolarispms.ui.property.PropertyListViewModel import androidx.compose.foundation.text.KeyboardOptions +import com.android.trisolarispms.ui.common.BackTopBarScaffold +import com.android.trisolarispms.ui.common.PaddedScreenColumn @Composable -@OptIn(ExperimentalMaterial3Api::class) fun HomeScreen( userId: String?, userName: String?, @@ -78,66 +74,57 @@ fun HomeScreen( } } - Scaffold( - topBar = { - TopAppBar( - title = { Text("Trisolaris PMS") }, - colors = TopAppBarDefaults.topAppBarColors(), - actions = { - if (isSuperAdmin) { - IconButton(onClick = onUserDirectory) { - Icon(Icons.Default.People, contentDescription = "User Directory") - } - } - IconButton(onClick = { menuExpanded = true }) { - Icon(Icons.Default.MoreVert, contentDescription = "Menu") - } - DropdownMenu( - expanded = menuExpanded, - onDismissRequest = { menuExpanded = false } - ) { - DropdownMenuItem( - text = { Text("Add Property") }, - onClick = { - menuExpanded = false - onAddProperty() - } - ) - DropdownMenuItem( - text = { Text("Logout") }, - onClick = { - menuExpanded = false - onLogout() - } - ) - if (isSuperAdmin) { - DropdownMenuItem( - text = { Text("Update Tags") }, - onClick = { - menuExpanded = false - onImageTags() - } - ) - DropdownMenuItem( - text = { Text("Modify Amenities") }, - onClick = { - menuExpanded = false - onAmenities() - } - ) - } - } + BackTopBarScaffold( + title = "Trisolaris PMS", + onBack = {}, + showBack = false, + actions = { + if (isSuperAdmin) { + IconButton(onClick = onUserDirectory) { + Icon(Icons.Default.People, contentDescription = "User Directory") } - ) + } + IconButton(onClick = { menuExpanded = true }) { + Icon(Icons.Default.MoreVert, contentDescription = "Menu") + } + DropdownMenu( + expanded = menuExpanded, + onDismissRequest = { menuExpanded = false } + ) { + DropdownMenuItem( + text = { Text("Add Property") }, + onClick = { + menuExpanded = false + onAddProperty() + } + ) + DropdownMenuItem( + text = { Text("Logout") }, + onClick = { + menuExpanded = false + onLogout() + } + ) + if (isSuperAdmin) { + DropdownMenuItem( + text = { Text("Update Tags") }, + onClick = { + menuExpanded = false + onImageTags() + } + ) + DropdownMenuItem( + text = { Text("Modify Amenities") }, + onClick = { + menuExpanded = false + onAmenities() + } + ) + } + } } ) { padding -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(padding) - .padding(24.dp), - verticalArrangement = Arrangement.Top - ) { + PaddedScreenColumn(padding = padding) { val title = if (!userName.isNullOrBlank()) "Welcome, $userName" else "Welcome" Text(text = title, style = MaterialTheme.typography.headlineMedium) if (isSuperAdmin) { diff --git a/app/src/main/java/com/android/trisolarispms/ui/property/AddPropertyScreen.kt b/app/src/main/java/com/android/trisolarispms/ui/property/AddPropertyScreen.kt index 5c786e6..adcb607 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/property/AddPropertyScreen.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/property/AddPropertyScreen.kt @@ -3,27 +3,18 @@ package com.android.trisolarispms.ui.property 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.foundation.text.KeyboardOptions -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuDefaults 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 @@ -44,6 +35,8 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch import java.util.UUID +import com.android.trisolarispms.ui.common.BackTopBarScaffold +import com.android.trisolarispms.ui.common.PaddedScreenColumn @Composable @OptIn(ExperimentalMaterial3Api::class) @@ -70,120 +63,105 @@ fun AddPropertyScreen( } } - Scaffold( - topBar = { - TopAppBar( - title = { Text("Add Property") }, - navigationIcon = { - IconButton(onClick = onBack) { - Icon(Icons.Default.ArrowBack, contentDescription = "Back") - } - }, - colors = TopAppBarDefaults.topAppBarColors() - ) - } + BackTopBarScaffold( + title = "Add Property", + onBack = onBack ) { padding -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(padding) - .padding(24.dp), - verticalArrangement = Arrangement.Top - ) { + PaddedScreenColumn(padding = padding) { Spacer(modifier = Modifier.height(8.dp)) - ExposedDropdownMenuBox( - expanded = expanded, - onExpandedChange = { expanded = it } - ) { - OutlinedTextField( - value = state.name, - onValueChange = { value -> - viewModel.onNameChange(value) - val query = value.trim() - searchJob?.cancel() - placesError = null - if (query.length >= 4) { - searchJob = coroutineScope.launch { - delay(300) - isSearching = true - try { - predictions = PlacesRestClient.autocomplete( - apiKey = apiKey, - input = query, - sessionToken = sessionToken, - regionCode = "IN" - ) - expanded = predictions.isNotEmpty() - } catch (e: Exception) { - predictions = emptyList() - expanded = false - placesError = e.localizedMessage ?: "Places search failed" - } finally { - isSearching = false - } - } - } else { - predictions = emptyList() - expanded = false - isSearching = false - } - }, - label = { Text("Name") }, - isError = state.nameError != null, - modifier = Modifier - .fillMaxWidth() - .menuAnchor(), - trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) } - ) - ExposedDropdownMenu( + ExposedDropdownMenuBox( expanded = expanded, - onDismissRequest = { expanded = false } + onExpandedChange = { expanded = it } ) { - predictions.forEach { prediction -> - DropdownMenuItem( - text = { Text(prediction.text) }, - onClick = { - expanded = false - predictions = emptyList() - coroutineScope.launch { + OutlinedTextField( + value = state.name, + onValueChange = { value -> + viewModel.onNameChange(value) + val query = value.trim() + searchJob?.cancel() + placesError = null + if (query.length >= 4) { + searchJob = coroutineScope.launch { + delay(300) + isSearching = true try { - val details = PlacesRestClient.placeDetails( + predictions = PlacesRestClient.autocomplete( apiKey = apiKey, - placeId = prediction.placeId, - sessionToken = sessionToken + input = query, + sessionToken = sessionToken, + regionCode = "IN" ) - viewModel.applyPlace(details.name, details.address) + expanded = predictions.isNotEmpty() } catch (e: Exception) { - placesError = e.localizedMessage ?: "Place lookup failed" + predictions = emptyList() + expanded = false + placesError = e.localizedMessage ?: "Places search failed" + } finally { + isSearching = false } } + } else { + predictions = emptyList() + expanded = false + isSearching = false } - ) + }, + label = { Text("Name") }, + isError = state.nameError != null, + modifier = Modifier + .fillMaxWidth() + .menuAnchor(), + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) } + ) + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + predictions.forEach { prediction -> + DropdownMenuItem( + text = { Text(prediction.text) }, + onClick = { + expanded = false + predictions = emptyList() + coroutineScope.launch { + try { + val details = PlacesRestClient.placeDetails( + apiKey = apiKey, + placeId = prediction.placeId, + sessionToken = sessionToken + ) + viewModel.applyPlace(details.name, details.address) + } catch (e: Exception) { + placesError = e.localizedMessage ?: "Place lookup failed" + } + } + } + ) + } } } - } - state.nameError?.let { - Spacer(modifier = Modifier.height(4.dp)) - Text(text = it, color = MaterialTheme.colorScheme.error) - } - if (isSearching) { - Spacer(modifier = Modifier.height(4.dp)) - Text(text = "Searching…", style = MaterialTheme.typography.bodySmall) - } - placesError?.let { - Spacer(modifier = Modifier.height(4.dp)) - Text(text = it, color = MaterialTheme.colorScheme.error) - } + state.nameError?.let { + Spacer(modifier = Modifier.height(4.dp)) + Text(text = it, color = MaterialTheme.colorScheme.error) + } + if (isSearching) { + Spacer(modifier = Modifier.height(4.dp)) + Text(text = "Searching…", style = MaterialTheme.typography.bodySmall) + } + placesError?.let { + Spacer(modifier = Modifier.height(4.dp)) + Text(text = it, color = MaterialTheme.colorScheme.error) + } - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(12.dp)) - OutlinedTextField( - value = state.addressText, - onValueChange = viewModel::onAddressChange, - label = { Text("Address") }, - modifier = Modifier.fillMaxWidth() - ) + OutlinedTextField( + value = state.addressText, + onValueChange = viewModel::onAddressChange, + label = { Text("Address") }, + modifier = Modifier.fillMaxWidth() + ) Spacer(modifier = Modifier.height(12.dp)) diff --git a/app/src/main/java/com/android/trisolarispms/ui/property/PropertyHomeScreen.kt b/app/src/main/java/com/android/trisolarispms/ui/property/PropertyHomeScreen.kt index d6ae09b..c83e063 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/property/PropertyHomeScreen.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/property/PropertyHomeScreen.kt @@ -4,27 +4,19 @@ 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.ArrowBack -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.Surface 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 +import com.android.trisolarispms.ui.common.BackTopBarScaffold +import com.android.trisolarispms.ui.common.PaddedScreenColumn @Composable -@OptIn(ExperimentalMaterial3Api::class) fun PropertyHomeScreen( propertyId: String, onBack: () -> Unit, @@ -33,26 +25,11 @@ fun PropertyHomeScreen( onRoomTypes: () -> Unit, canManageRooms: Boolean ) { - Scaffold( - topBar = { - TopAppBar( - title = { Text("Property") }, - navigationIcon = { - IconButton(onClick = onBack) { - Icon(Icons.Default.ArrowBack, contentDescription = "Back") - } - }, - colors = TopAppBarDefaults.topAppBarColors() - ) - } + BackTopBarScaffold( + title = "Property", + onBack = onBack ) { padding -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(padding) - .padding(24.dp), - verticalArrangement = Arrangement.Top - ) { + PaddedScreenColumn(padding = padding) { PropertyTile(title = "Checked-in Rooms", subtitle = "Active stays", onClick = onActiveStays) Spacer(modifier = Modifier.height(12.dp)) PropertyTile(title = "Available Rooms", subtitle = "Room list", onClick = onRooms) diff --git a/app/src/main/java/com/android/trisolarispms/ui/razorpay/RazorpayQrScreen.kt b/app/src/main/java/com/android/trisolarispms/ui/razorpay/RazorpayQrScreen.kt index 155c54b..18a1ecc 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/razorpay/RazorpayQrScreen.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/razorpay/RazorpayQrScreen.kt @@ -16,19 +16,14 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.clickable import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Delete import androidx.compose.material3.Button 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.OutlinedTextField -import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -49,9 +44,9 @@ import com.android.trisolarispms.BuildConfig import androidx.compose.ui.platform.LocalContext import android.content.Intent import android.net.Uri +import com.android.trisolarispms.ui.common.BackTopBarScaffold @Composable -@OptIn(ExperimentalMaterial3Api::class) fun RazorpayQrScreen( propertyId: String, bookingId: String, @@ -93,23 +88,14 @@ fun RazorpayQrScreen( exitAndRefresh() } - Scaffold( - topBar = { - TopAppBar( - title = { Text("Generate Payment Links | QR") }, - navigationIcon = { - IconButton(onClick = { - if (isViewingQr) { - exitAndRefresh() - } else { - onBack() - } - }) { - Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = "Back") - } - }, - colors = TopAppBarDefaults.topAppBarColors() - ) + BackTopBarScaffold( + title = "Generate Payment Links | QR", + onBack = { + if (isViewingQr) { + exitAndRefresh() + } else { + onBack() + } } ) { padding -> if (state.isCredited) { diff --git a/app/src/main/java/com/android/trisolarispms/ui/roomstay/ActiveRoomStaysScreen.kt b/app/src/main/java/com/android/trisolarispms/ui/roomstay/ActiveRoomStaysScreen.kt index ad9c7f0..7f89ffd 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/roomstay/ActiveRoomStaysScreen.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/roomstay/ActiveRoomStaysScreen.kt @@ -3,7 +3,6 @@ package com.android.trisolarispms.ui.roomstay 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 @@ -13,7 +12,6 @@ import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.combinedClickable import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.People import androidx.compose.material.icons.filled.MeetingRoom import androidx.compose.material.icons.filled.Payment @@ -23,15 +21,11 @@ import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.FloatingActionButton 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.TextButton -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.material3.LinearProgressIndicator @@ -43,11 +37,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.android.trisolarispms.data.api.model.BookingListItem +import com.android.trisolarispms.ui.common.BackTopBarScaffold +import com.android.trisolarispms.ui.common.PaddedScreenColumn import java.time.Duration import java.time.OffsetDateTime @Composable -@OptIn(ExperimentalMaterial3Api::class) fun ActiveRoomStaysScreen( propertyId: String, propertyName: String, @@ -73,37 +68,27 @@ fun ActiveRoomStaysScreen( viewModel.load(propertyId) } - Scaffold( - topBar = { - TopAppBar( - title = { Text(propertyName) }, - navigationIcon = { - if (showBack) { - IconButton(onClick = onBack) { - Icon(Icons.Default.ArrowBack, contentDescription = "Back") - } - } - }, - actions = { - IconButton(onClick = onViewRooms) { - Icon(Icons.Default.MeetingRoom, contentDescription = "Available Rooms") - } - IconButton(onClick = onOpenSettings) { - Icon(Icons.Default.Settings, contentDescription = "Settings") - } - if (showRazorpaySettings) { - IconButton(onClick = onRazorpaySettings) { - Icon(Icons.Default.Payment, contentDescription = "Razorpay Settings") - } - } - if (showUserAdmin) { - IconButton(onClick = onUserAdmin) { - Icon(Icons.Default.People, contentDescription = "Property Users") - } - } - }, - colors = TopAppBarDefaults.topAppBarColors() - ) + BackTopBarScaffold( + title = propertyName, + onBack = onBack, + showBack = showBack, + actions = { + IconButton(onClick = onViewRooms) { + Icon(Icons.Default.MeetingRoom, contentDescription = "Available Rooms") + } + IconButton(onClick = onOpenSettings) { + Icon(Icons.Default.Settings, contentDescription = "Settings") + } + if (showRazorpaySettings) { + IconButton(onClick = onRazorpaySettings) { + Icon(Icons.Default.Payment, contentDescription = "Razorpay Settings") + } + } + if (showUserAdmin) { + IconButton(onClick = onUserAdmin) { + Icon(Icons.Default.People, contentDescription = "Property Users") + } + } }, floatingActionButton = { if (canCreateBooking) { @@ -113,13 +98,7 @@ fun ActiveRoomStaysScreen( } } ) { padding -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(padding) - .padding(24.dp), - verticalArrangement = Arrangement.Top - ) { + PaddedScreenColumn(padding = padding) { Spacer(modifier = Modifier.height(8.dp)) if (state.isLoading) { diff --git a/app/src/main/java/com/android/trisolarispms/ui/roomstay/ManageRoomStayRatesScreen.kt b/app/src/main/java/com/android/trisolarispms/ui/roomstay/ManageRoomStayRatesScreen.kt index 5484a48..a428d47 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/roomstay/ManageRoomStayRatesScreen.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/roomstay/ManageRoomStayRatesScreen.kt @@ -5,25 +5,16 @@ 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.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button 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.OutlinedTextField -import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextButton -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 @@ -36,9 +27,10 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import androidx.compose.foundation.text.KeyboardOptions +import com.android.trisolarispms.ui.common.BackTopBarScaffold +import com.android.trisolarispms.ui.common.PaddedScreenColumn @Composable -@OptIn(ExperimentalMaterial3Api::class) fun ManageRoomStayRatesScreen( propertyId: String, bookingId: String, @@ -57,18 +49,9 @@ fun ManageRoomStayRatesScreen( viewModel.setItems(selectedRooms) } - Scaffold( - topBar = { - TopAppBar( - title = { Text("Selected Rooms") }, - navigationIcon = { - IconButton(onClick = onBack) { - Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = "Back") - } - }, - colors = TopAppBarDefaults.topAppBarColors() - ) - }, + BackTopBarScaffold( + title = "Selected Rooms", + onBack = onBack, bottomBar = { Column( modifier = Modifier @@ -102,16 +85,13 @@ fun ManageRoomStayRatesScreen( } } ) { padding -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(padding) - .padding(16.dp), - verticalArrangement = Arrangement.Top + PaddedScreenColumn( + padding = padding, + contentPadding = 16.dp ) { if (state.items.isEmpty()) { Text(text = "No rooms selected.") - return@Column + return@PaddedScreenColumn } state.items.forEach { item -> Row( diff --git a/app/src/main/java/com/android/trisolarispms/ui/roomstay/ManageRoomStaySelectScreen.kt b/app/src/main/java/com/android/trisolarispms/ui/roomstay/ManageRoomStaySelectScreen.kt index f8fa113..dc2c9a2 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/roomstay/ManageRoomStaySelectScreen.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/roomstay/ManageRoomStaySelectScreen.kt @@ -17,18 +17,10 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.Button 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 @@ -41,11 +33,13 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.android.trisolarispms.data.api.model.RoomAvailableRateResponse +import com.android.trisolarispms.ui.common.BackTopBarScaffold +import com.android.trisolarispms.ui.common.LoadingAndError +import com.android.trisolarispms.ui.common.PaddedScreenColumn import java.time.OffsetDateTime import java.time.format.DateTimeFormatter @Composable -@OptIn(ExperimentalMaterial3Api::class) fun ManageRoomStaySelectScreen( propertyId: String, bookingFromAt: String, @@ -76,18 +70,9 @@ fun ManageRoomStaySelectScreen( } } - Scaffold( - topBar = { - TopAppBar( - title = { Text("Select Rooms") }, - navigationIcon = { - IconButton(onClick = onBack) { - Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = "Back") - } - }, - colors = TopAppBarDefaults.topAppBarColors() - ) - }, + BackTopBarScaffold( + title = "Select Rooms", + onBack = onBack, bottomBar = { Column( modifier = Modifier @@ -112,23 +97,14 @@ fun ManageRoomStaySelectScreen( } } ) { padding -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(padding) - .padding(16.dp) + PaddedScreenColumn( + padding = padding, + contentPadding = 16.dp ) { - 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)) - } + LoadingAndError(isLoading = state.isLoading, error = state.error) if (fromDate == null || fallbackToDate == null) { Text(text = "Booking dates not available.", color = MaterialTheme.colorScheme.error) - return@Column + return@PaddedScreenColumn } LazyVerticalGrid( columns = GridCells.Fixed(2), diff --git a/app/src/main/java/com/android/trisolarispms/ui/roomtype/RoomTypeFormScreen.kt b/app/src/main/java/com/android/trisolarispms/ui/roomtype/RoomTypeFormScreen.kt index 287b3ed..9c8d009 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/roomtype/RoomTypeFormScreen.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/roomtype/RoomTypeFormScreen.kt @@ -16,22 +16,16 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.Delete -import androidx.compose.material.icons.filled.Done import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button 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.TextButton -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 @@ -45,9 +39,9 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import coil.compose.AsyncImage import com.android.trisolarispms.data.api.core.ApiConstants +import com.android.trisolarispms.ui.common.SaveTopBarScaffold @Composable -@OptIn(ExperimentalMaterial3Api::class) fun RoomTypeFormScreen( title: String, propertyId: String, @@ -68,27 +62,16 @@ fun RoomTypeFormScreen( amenityViewModel.load() } - Scaffold( - topBar = { - TopAppBar( - title = { Text(title) }, - navigationIcon = { - IconButton(onClick = onBack) { - Icon(Icons.Default.ArrowBack, contentDescription = "Back") - } - }, - actions = { - IconButton(onClick = onSave) { - Icon(Icons.Default.Done, contentDescription = "Save") - } - if (onDelete != null) { - IconButton(onClick = onDelete) { - Icon(Icons.Default.Delete, contentDescription = "Delete Room Type") - } - } - }, - colors = TopAppBarDefaults.topAppBarColors() - ) + SaveTopBarScaffold( + title = title, + onBack = onBack, + onSave = onSave, + actions = { + if (onDelete != null) { + IconButton(onClick = onDelete) { + Icon(Icons.Default.Delete, contentDescription = "Delete Room Type") + } + } } ) { padding -> Column( diff --git a/app/src/main/java/com/android/trisolarispms/ui/users/UserDirectoryScaffold.kt b/app/src/main/java/com/android/trisolarispms/ui/users/UserDirectoryScaffold.kt index 79e9c9b..0b7cc82 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/users/UserDirectoryScaffold.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/users/UserDirectoryScaffold.kt @@ -3,36 +3,30 @@ package com.android.trisolarispms.ui.users 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.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.VpnKey import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button -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.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.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import com.android.trisolarispms.ui.common.BackTopBarScaffold +import com.android.trisolarispms.ui.common.LoadingAndError +import com.android.trisolarispms.ui.common.PaddedScreenColumn @Composable -@OptIn(ExperimentalMaterial3Api::class) fun UserDirectoryScaffold( title: String, onBack: () -> Unit, @@ -47,48 +41,29 @@ fun UserDirectoryScaffold( beforeListContent: @Composable () -> Unit = {}, itemContent: @Composable (PropertyUserUi) -> Unit ) { - Scaffold( - topBar = { - TopAppBar( - title = { Text(title) }, - navigationIcon = { - IconButton(onClick = onBack) { - Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = "Back") - } - }, - actions = { - if (showAccessCodeIcon) { - IconButton(onClick = onAccessCodeClick) { - Icon(Icons.Default.VpnKey, contentDescription = "Generate access code") - } - } - if (showSearchIcon) { - IconButton(onClick = onSearchClick) { - Icon(Icons.Default.Search, contentDescription = "Search users") - } - } - }, - colors = TopAppBarDefaults.topAppBarColors() - ) + BackTopBarScaffold( + title = title, + onBack = onBack, + actions = { + if (showAccessCodeIcon) { + IconButton(onClick = onAccessCodeClick) { + Icon(Icons.Default.VpnKey, contentDescription = "Generate access code") + } + } + if (showSearchIcon) { + IconButton(onClick = onSearchClick) { + Icon(Icons.Default.Search, contentDescription = "Search users") + } + } } ) { padding -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(padding) - .padding(16.dp), - verticalArrangement = Arrangement.Top + PaddedScreenColumn( + padding = padding, + contentPadding = 16.dp ) { beforeListContent() - if (isLoading) { - Spacer(modifier = Modifier.height(12.dp)) - CircularProgressIndicator() - } - error?.let { - Spacer(modifier = Modifier.height(12.dp)) - Text(text = it, color = MaterialTheme.colorScheme.error) - } + LoadingAndError(isLoading = isLoading, error = error) Spacer(modifier = Modifier.height(12.dp)) LazyColumn(verticalArrangement = Arrangement.spacedBy(12.dp)) {