Compare commits
2 Commits
c5e0648dd1
...
9ac0b55b89
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ac0b55b89 | ||
|
|
4fc080f146 |
@@ -3,6 +3,7 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
<uses-permission android:name="android.permission.NFC" />
|
<uses-permission android:name="android.permission.NFC" />
|
||||||
|
|
||||||
<uses-feature
|
<uses-feature
|
||||||
|
|||||||
@@ -91,6 +91,9 @@ class MainActivity : ComponentActivity() {
|
|||||||
it == "ADMIN" || it == "MANAGER"
|
it == "ADMIN" || it == "MANAGER"
|
||||||
} == true
|
} == true
|
||||||
}
|
}
|
||||||
|
val canDeleteCashPayment: (String) -> Boolean = { propertyId ->
|
||||||
|
state.isSuperAdmin || state.propertyRoles[propertyId]?.contains("ADMIN") == true
|
||||||
|
}
|
||||||
|
|
||||||
BackHandler(enabled = currentRoute != AppRoute.Home) {
|
BackHandler(enabled = currentRoute != AppRoute.Home) {
|
||||||
when (currentRoute) {
|
when (currentRoute) {
|
||||||
@@ -480,12 +483,14 @@ class MainActivity : ComponentActivity() {
|
|||||||
propertyId = currentRoute.propertyId,
|
propertyId = currentRoute.propertyId,
|
||||||
bookingId = currentRoute.bookingId
|
bookingId = currentRoute.bookingId
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
canManageDocuments = canManagePayuSettings(currentRoute.propertyId)
|
||||||
)
|
)
|
||||||
is AppRoute.BookingPayments -> BookingPaymentsScreen(
|
is AppRoute.BookingPayments -> BookingPaymentsScreen(
|
||||||
propertyId = currentRoute.propertyId,
|
propertyId = currentRoute.propertyId,
|
||||||
bookingId = currentRoute.bookingId,
|
bookingId = currentRoute.bookingId,
|
||||||
canAddCash = canManagePayuSettings(currentRoute.propertyId),
|
canAddCash = canManagePayuSettings(currentRoute.propertyId),
|
||||||
|
canDeleteCash = canDeleteCashPayment(currentRoute.propertyId),
|
||||||
onBack = {
|
onBack = {
|
||||||
route.value = AppRoute.BookingDetailsTabs(
|
route.value = AppRoute.BookingDetailsTabs(
|
||||||
currentRoute.propertyId,
|
currentRoute.propertyId,
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import com.android.trisolarispms.data.api.model.PaymentCreateRequest
|
|||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
import retrofit2.http.Body
|
import retrofit2.http.Body
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.DELETE
|
||||||
import retrofit2.http.POST
|
import retrofit2.http.POST
|
||||||
import retrofit2.http.Path
|
import retrofit2.http.Path
|
||||||
import retrofit2.http.Query
|
import retrofit2.http.Query
|
||||||
@@ -128,4 +129,11 @@ interface BookingApi {
|
|||||||
@Path("bookingId") bookingId: String,
|
@Path("bookingId") bookingId: String,
|
||||||
@Body body: PaymentCreateRequest
|
@Body body: PaymentCreateRequest
|
||||||
): Response<PaymentDto>
|
): Response<PaymentDto>
|
||||||
|
|
||||||
|
@DELETE("properties/{propertyId}/bookings/{bookingId}/payments/{paymentId}")
|
||||||
|
suspend fun deletePayment(
|
||||||
|
@Path("propertyId") propertyId: String,
|
||||||
|
@Path("bookingId") bookingId: String,
|
||||||
|
@Path("paymentId") paymentId: String
|
||||||
|
): Response<Unit>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ import okhttp3.ResponseBody
|
|||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
import retrofit2.http.Multipart
|
import retrofit2.http.Multipart
|
||||||
|
import retrofit2.http.DELETE
|
||||||
import retrofit2.http.POST
|
import retrofit2.http.POST
|
||||||
import retrofit2.http.Part
|
import retrofit2.http.Part
|
||||||
import retrofit2.http.Path
|
import retrofit2.http.Path
|
||||||
|
import retrofit2.http.Query
|
||||||
import retrofit2.http.Streaming
|
import retrofit2.http.Streaming
|
||||||
|
|
||||||
interface GuestDocumentApi {
|
interface GuestDocumentApi {
|
||||||
@@ -22,6 +24,15 @@ interface GuestDocumentApi {
|
|||||||
@Part("bookingId") bookingId: RequestBody
|
@Part("bookingId") bookingId: RequestBody
|
||||||
): Response<GuestDocumentDto>
|
): Response<GuestDocumentDto>
|
||||||
|
|
||||||
|
@Multipart
|
||||||
|
@POST("properties/{propertyId}/guests/{guestId}/documents")
|
||||||
|
suspend fun uploadGuestDocumentWithBooking(
|
||||||
|
@Path("propertyId") propertyId: String,
|
||||||
|
@Path("guestId") guestId: String,
|
||||||
|
@Query("bookingId") bookingId: String,
|
||||||
|
@Part file: MultipartBody.Part
|
||||||
|
): Response<GuestDocumentDto>
|
||||||
|
|
||||||
@GET("properties/{propertyId}/guests/{guestId}/documents")
|
@GET("properties/{propertyId}/guests/{guestId}/documents")
|
||||||
suspend fun listGuestDocuments(
|
suspend fun listGuestDocuments(
|
||||||
@Path("propertyId") propertyId: String,
|
@Path("propertyId") propertyId: String,
|
||||||
@@ -35,4 +46,11 @@ interface GuestDocumentApi {
|
|||||||
@Path("guestId") guestId: String,
|
@Path("guestId") guestId: String,
|
||||||
@Path("documentId") documentId: String
|
@Path("documentId") documentId: String
|
||||||
): Response<ResponseBody>
|
): Response<ResponseBody>
|
||||||
|
|
||||||
|
@DELETE("properties/{propertyId}/guests/{guestId}/documents/{documentId}")
|
||||||
|
suspend fun deleteGuestDocument(
|
||||||
|
@Path("propertyId") propertyId: String,
|
||||||
|
@Path("guestId") guestId: String,
|
||||||
|
@Path("documentId") documentId: String
|
||||||
|
): Response<Unit>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.android.trisolarispms.ui.guestdocs
|
||||||
|
|
||||||
|
import com.android.trisolarispms.data.api.model.GuestDocumentDto
|
||||||
|
|
||||||
|
data class GuestDocumentsState(
|
||||||
|
val isLoading: Boolean = false,
|
||||||
|
val isUploading: Boolean = false,
|
||||||
|
val error: String? = null,
|
||||||
|
val documents: List<GuestDocumentDto> = emptyList()
|
||||||
|
)
|
||||||
@@ -0,0 +1,341 @@
|
|||||||
|
package com.android.trisolarispms.ui.guestdocs
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
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.PaddingValues
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.FloatingActionButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import coil.ImageLoader
|
||||||
|
import coil.compose.SubcomposeAsyncImage
|
||||||
|
import coil.decode.SvgDecoder
|
||||||
|
import coil.request.ImageRequest
|
||||||
|
import com.android.trisolarispms.data.api.ApiConstants
|
||||||
|
import com.android.trisolarispms.data.api.FirebaseAuthTokenProvider
|
||||||
|
import com.android.trisolarispms.data.api.model.GuestDocumentDto
|
||||||
|
import com.google.firebase.auth.FirebaseAuth
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import androidx.compose.animation.core.LinearEasing
|
||||||
|
import androidx.compose.animation.core.RepeatMode
|
||||||
|
import androidx.compose.animation.core.animateFloat
|
||||||
|
import androidx.compose.animation.core.infiniteRepeatable
|
||||||
|
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
fun GuestDocumentsTab(
|
||||||
|
propertyId: String,
|
||||||
|
guestId: String,
|
||||||
|
bookingId: String,
|
||||||
|
canManageDocuments: Boolean,
|
||||||
|
viewModel: GuestDocumentsViewModel = viewModel()
|
||||||
|
) {
|
||||||
|
val state by viewModel.state.collectAsState()
|
||||||
|
val context = LocalContext.current
|
||||||
|
val showPicker = remember { mutableStateOf(false) }
|
||||||
|
val auth = remember { FirebaseAuth.getInstance() }
|
||||||
|
val tokenProvider = remember { FirebaseAuthTokenProvider(auth) }
|
||||||
|
val imageLoader = remember {
|
||||||
|
ImageLoader.Builder(context)
|
||||||
|
.components { add(SvgDecoder.Factory()) }
|
||||||
|
.okHttpClient(
|
||||||
|
OkHttpClient.Builder()
|
||||||
|
.addInterceptor(Interceptor { chain ->
|
||||||
|
val original = chain.request()
|
||||||
|
val token = runCatching {
|
||||||
|
kotlinx.coroutines.runBlocking { tokenProvider.token(forceRefresh = false) }
|
||||||
|
}.getOrNull()
|
||||||
|
if (token.isNullOrBlank()) {
|
||||||
|
chain.proceed(original)
|
||||||
|
} else {
|
||||||
|
val request = original.newBuilder()
|
||||||
|
.addHeader("Authorization", "Bearer $token")
|
||||||
|
.build()
|
||||||
|
chain.proceed(request)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(propertyId, guestId) {
|
||||||
|
viewModel.load(propertyId, guestId)
|
||||||
|
}
|
||||||
|
|
||||||
|
val cameraLauncher = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.TakePicturePreview()
|
||||||
|
) { bitmap: Bitmap? ->
|
||||||
|
if (bitmap != null) {
|
||||||
|
val file = writeBitmapToCache(bitmap, context.cacheDir)
|
||||||
|
viewModel.uploadFromFile(
|
||||||
|
propertyId = propertyId,
|
||||||
|
guestId = guestId,
|
||||||
|
bookingId = bookingId,
|
||||||
|
file = file,
|
||||||
|
mimeType = "image/jpeg"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val uploadLauncher = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.OpenMultipleDocuments()
|
||||||
|
) { uris ->
|
||||||
|
if (uris.isNotEmpty()) {
|
||||||
|
viewModel.uploadFromUris(
|
||||||
|
context = context,
|
||||||
|
propertyId = propertyId,
|
||||||
|
guestId = guestId,
|
||||||
|
bookingId = bookingId,
|
||||||
|
uris = uris
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
if (state.isLoading) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
if (state.isUploading) {
|
||||||
|
Text(text = "Uploading...", style = MaterialTheme.typography.bodySmall)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
state.error?.let {
|
||||||
|
Text(text = it, color = MaterialTheme.colorScheme.error)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
if (!canManageDocuments) {
|
||||||
|
Text(text = "You don't have access to view documents.")
|
||||||
|
return@Column
|
||||||
|
}
|
||||||
|
if (!state.isLoading && state.documents.isEmpty()) {
|
||||||
|
Text(text = "No documents yet")
|
||||||
|
}
|
||||||
|
val imageDocs = state.documents.filter {
|
||||||
|
it.contentType?.lowercase()?.startsWith("image/") == true
|
||||||
|
}
|
||||||
|
LazyVerticalGrid(
|
||||||
|
columns = GridCells.Fixed(2),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
contentPadding = PaddingValues(bottom = 80.dp)
|
||||||
|
) {
|
||||||
|
if (state.isLoading && imageDocs.isEmpty()) {
|
||||||
|
items(4) {
|
||||||
|
ShimmerTile()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
items(imageDocs) { doc ->
|
||||||
|
DocumentImageTile(
|
||||||
|
propertyId = propertyId,
|
||||||
|
guestId = guestId,
|
||||||
|
doc = doc,
|
||||||
|
imageLoader = imageLoader,
|
||||||
|
canDelete = canManageDocuments,
|
||||||
|
onDelete = { documentId ->
|
||||||
|
viewModel.deleteDocument(propertyId, guestId, documentId)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canManageDocuments) {
|
||||||
|
FloatingActionButton(
|
||||||
|
onClick = { showPicker.value = true },
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomEnd)
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
androidx.compose.material3.Icon(
|
||||||
|
imageVector = Icons.Default.Add,
|
||||||
|
contentDescription = "Add document"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showPicker.value) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showPicker.value = false },
|
||||||
|
title = { Text("Add document") },
|
||||||
|
text = { Text("Choose camera or upload") },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
showPicker.value = false
|
||||||
|
cameraLauncher.launch(null)
|
||||||
|
}
|
||||||
|
) { Text("Camera") }
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
showPicker.value = false
|
||||||
|
uploadLauncher.launch(arrayOf("*/*"))
|
||||||
|
}
|
||||||
|
) { Text("Upload") }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DocumentImageTile(
|
||||||
|
propertyId: String,
|
||||||
|
guestId: String,
|
||||||
|
doc: GuestDocumentDto,
|
||||||
|
imageLoader: ImageLoader,
|
||||||
|
canDelete: Boolean,
|
||||||
|
onDelete: (String) -> Unit
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val documentId = doc.id ?: return
|
||||||
|
val baseUrl = ApiConstants.BASE_URL.trimEnd('/')
|
||||||
|
val url = "$baseUrl/properties/$propertyId/guests/$guestId/documents/$documentId/file"
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(140.dp)
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
) {
|
||||||
|
SubcomposeAsyncImage(
|
||||||
|
model = ImageRequest.Builder(context).data(url).build(),
|
||||||
|
imageLoader = imageLoader,
|
||||||
|
contentDescription = "Document image",
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
loading = { ShimmerOverlay() },
|
||||||
|
error = { ShimmerOverlay() }
|
||||||
|
)
|
||||||
|
if (canDelete) {
|
||||||
|
IconButton(
|
||||||
|
onClick = { onDelete(documentId) },
|
||||||
|
modifier = Modifier.align(Alignment.TopEnd)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Delete,
|
||||||
|
contentDescription = "Delete document",
|
||||||
|
tint = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val docType = doc.extractedData?.get("docType")
|
||||||
|
if (!docType.isNullOrBlank()) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomStart)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(MaterialTheme.colorScheme.scrim.copy(alpha = 0.45f))
|
||||||
|
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = docType,
|
||||||
|
color = MaterialTheme.colorScheme.onPrimary,
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun writeBitmapToCache(bitmap: Bitmap, cacheDir: File): File {
|
||||||
|
val file = File(cacheDir, "doc_${System.currentTimeMillis()}.jpg")
|
||||||
|
FileOutputStream(file).use { stream ->
|
||||||
|
bitmap.compress(Bitmap.CompressFormat.JPEG, 92, stream)
|
||||||
|
}
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ShimmerTile() {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(140.dp)
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
) {
|
||||||
|
ShimmerOverlay()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ShimmerOverlay() {
|
||||||
|
val transition = rememberInfiniteTransition(label = "shimmer")
|
||||||
|
val x = transition.animateFloat(
|
||||||
|
initialValue = 0f,
|
||||||
|
targetValue = 1000f,
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(durationMillis = 1000, easing = LinearEasing),
|
||||||
|
repeatMode = RepeatMode.Restart
|
||||||
|
),
|
||||||
|
label = "shimmerX"
|
||||||
|
)
|
||||||
|
val brush = Brush.linearGradient(
|
||||||
|
colors = listOf(
|
||||||
|
MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.08f),
|
||||||
|
MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
),
|
||||||
|
start = androidx.compose.ui.geometry.Offset(x.value - 200f, 0f),
|
||||||
|
end = androidx.compose.ui.geometry.Offset(x.value, 200f)
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(brush)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
package com.android.trisolarispms.ui.guestdocs
|
||||||
|
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.android.trisolarispms.data.api.ApiClient
|
||||||
|
import com.android.trisolarispms.data.api.model.GuestDocumentDto
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
|
import okhttp3.MultipartBody
|
||||||
|
import okhttp3.RequestBody.Companion.asRequestBody
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class GuestDocumentsViewModel : ViewModel() {
|
||||||
|
private val _state = MutableStateFlow(GuestDocumentsState())
|
||||||
|
val state: StateFlow<GuestDocumentsState> = _state
|
||||||
|
|
||||||
|
fun load(propertyId: String, guestId: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_state.update { it.copy(isLoading = true, error = null) }
|
||||||
|
try {
|
||||||
|
val api = ApiClient.create()
|
||||||
|
val response = api.listGuestDocuments(propertyId, guestId)
|
||||||
|
val body = response.body()
|
||||||
|
if (response.isSuccessful && body != null) {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
documents = body,
|
||||||
|
error = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
error = "Load failed: ${response.code()}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
error = e.localizedMessage ?: "Load failed"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun uploadFromUri(
|
||||||
|
context: Context,
|
||||||
|
propertyId: String,
|
||||||
|
guestId: String,
|
||||||
|
bookingId: String,
|
||||||
|
uri: Uri
|
||||||
|
) {
|
||||||
|
uploadFromUris(context, propertyId, guestId, bookingId, listOf(uri))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun uploadFromUris(
|
||||||
|
context: Context,
|
||||||
|
propertyId: String,
|
||||||
|
guestId: String,
|
||||||
|
bookingId: String,
|
||||||
|
uris: List<Uri>
|
||||||
|
) {
|
||||||
|
if (uris.isEmpty()) return
|
||||||
|
viewModelScope.launch {
|
||||||
|
_state.update { it.copy(isUploading = true, error = null) }
|
||||||
|
var errorMessage: String? = null
|
||||||
|
val resolver = context.contentResolver
|
||||||
|
for (uri in uris) {
|
||||||
|
try {
|
||||||
|
val mime = resolver.getType(uri) ?: "application/octet-stream"
|
||||||
|
if (mime.lowercase(Locale.getDefault()).startsWith("video/")) {
|
||||||
|
errorMessage = "Video files not allowed"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val filename = resolveFileName(resolver, uri) ?: "document"
|
||||||
|
val file = copyToCache(resolver, uri, context.cacheDir, filename)
|
||||||
|
uploadFile(propertyId, guestId, bookingId, file, mime)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
errorMessage = e.localizedMessage ?: "Upload failed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isUploading = false,
|
||||||
|
error = errorMessage
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteDocument(propertyId: String, guestId: String, documentId: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_state.update { it.copy(isLoading = true, error = null) }
|
||||||
|
try {
|
||||||
|
val api = ApiClient.create()
|
||||||
|
val response = api.deleteGuestDocument(propertyId, guestId, documentId)
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
_state.update { current ->
|
||||||
|
current.copy(
|
||||||
|
isLoading = false,
|
||||||
|
documents = current.documents.filterNot { it.id == documentId },
|
||||||
|
error = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
error = "Delete failed: ${response.code()}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
error = e.localizedMessage ?: "Delete failed"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun uploadFromFile(
|
||||||
|
propertyId: String,
|
||||||
|
guestId: String,
|
||||||
|
bookingId: String,
|
||||||
|
file: File,
|
||||||
|
mimeType: String
|
||||||
|
) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_state.update { it.copy(isUploading = true, error = null) }
|
||||||
|
try {
|
||||||
|
uploadFile(propertyId, guestId, bookingId, file, mimeType)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isUploading = false,
|
||||||
|
error = e.localizedMessage ?: "Upload failed"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun uploadFile(
|
||||||
|
propertyId: String,
|
||||||
|
guestId: String,
|
||||||
|
bookingId: String,
|
||||||
|
file: File,
|
||||||
|
mimeType: String
|
||||||
|
) {
|
||||||
|
val requestBody = file.asRequestBody(mimeType.toMediaTypeOrNull())
|
||||||
|
val part = MultipartBody.Part.createFormData("file", file.name, requestBody)
|
||||||
|
val api = ApiClient.create()
|
||||||
|
val response = api.uploadGuestDocumentWithBooking(
|
||||||
|
propertyId = propertyId,
|
||||||
|
guestId = guestId,
|
||||||
|
bookingId = bookingId,
|
||||||
|
file = part
|
||||||
|
)
|
||||||
|
val body = response.body()
|
||||||
|
if (response.isSuccessful && body != null) {
|
||||||
|
_state.update { current ->
|
||||||
|
current.copy(
|
||||||
|
isUploading = false,
|
||||||
|
documents = listOf(body) + current.documents,
|
||||||
|
error = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isUploading = false,
|
||||||
|
error = "Upload failed: ${response.code()}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolveFileName(resolver: ContentResolver, uri: Uri): String? {
|
||||||
|
val cursor = resolver.query(uri, null, null, null, null) ?: return null
|
||||||
|
return cursor.use {
|
||||||
|
val nameIndex = it.getColumnIndex(android.provider.OpenableColumns.DISPLAY_NAME)
|
||||||
|
if (nameIndex >= 0 && it.moveToFirst()) it.getString(nameIndex) else null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun copyToCache(
|
||||||
|
resolver: ContentResolver,
|
||||||
|
uri: Uri,
|
||||||
|
cacheDir: File,
|
||||||
|
filename: String
|
||||||
|
): File {
|
||||||
|
val output = File(cacheDir, "${System.currentTimeMillis()}_$filename")
|
||||||
|
resolver.openInputStream(uri)?.use { input ->
|
||||||
|
FileOutputStream(output).use { outputStream ->
|
||||||
|
input.copyTo(outputStream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package com.android.trisolarispms.ui.payment
|
|||||||
|
|
||||||
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
|
||||||
@@ -12,6 +13,7 @@ import androidx.compose.foundation.lazy.items
|
|||||||
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.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
@@ -31,6 +33,7 @@ import androidx.compose.runtime.collectAsState
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -45,6 +48,7 @@ fun BookingPaymentsScreen(
|
|||||||
propertyId: String,
|
propertyId: String,
|
||||||
bookingId: String,
|
bookingId: String,
|
||||||
canAddCash: Boolean,
|
canAddCash: Boolean,
|
||||||
|
canDeleteCash: Boolean,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
viewModel: BookingPaymentsViewModel = viewModel()
|
viewModel: BookingPaymentsViewModel = viewModel()
|
||||||
) {
|
) {
|
||||||
@@ -87,6 +91,7 @@ fun BookingPaymentsScreen(
|
|||||||
onClick = {
|
onClick = {
|
||||||
val amount = amountInput.value.toLongOrNull() ?: 0L
|
val amount = amountInput.value.toLongOrNull() ?: 0L
|
||||||
viewModel.addCashPayment(propertyId, bookingId, amount)
|
viewModel.addCashPayment(propertyId, bookingId, amount)
|
||||||
|
amountInput.value = ""
|
||||||
},
|
},
|
||||||
enabled = !state.isLoading,
|
enabled = !state.isLoading,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
@@ -108,10 +113,18 @@ fun BookingPaymentsScreen(
|
|||||||
}
|
}
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f)
|
||||||
) {
|
) {
|
||||||
items(state.payments) { payment ->
|
items(state.payments) { payment ->
|
||||||
PaymentCard(payment = payment)
|
PaymentCard(
|
||||||
|
payment = payment,
|
||||||
|
canDeleteCash = canDeleteCash,
|
||||||
|
onDelete = { paymentId ->
|
||||||
|
viewModel.deleteCashPayment(propertyId, bookingId, paymentId)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,7 +132,11 @@ fun BookingPaymentsScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun PaymentCard(payment: PaymentDto) {
|
private fun PaymentCard(
|
||||||
|
payment: PaymentDto,
|
||||||
|
canDeleteCash: Boolean,
|
||||||
|
onDelete: (String) -> Unit
|
||||||
|
) {
|
||||||
val date = payment.receivedAt?.let {
|
val date = payment.receivedAt?.let {
|
||||||
runCatching { OffsetDateTime.parse(it) }.getOrNull()
|
runCatching { OffsetDateTime.parse(it) }.getOrNull()
|
||||||
}
|
}
|
||||||
@@ -136,19 +153,52 @@ private fun PaymentCard(payment: PaymentDto) {
|
|||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(12.dp)) {
|
Column(modifier = Modifier.padding(12.dp)) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
val amountText = buildString {
|
val amountText = buildString {
|
||||||
append(payment.amount?.toString() ?: "-")
|
append(payment.amount?.toString() ?: "-")
|
||||||
payment.currency?.let { append(" $it") }
|
payment.currency?.let { append(" $it") }
|
||||||
}
|
}
|
||||||
Text(text = amountText, style = MaterialTheme.typography.titleMedium)
|
Text(text = amountText, style = MaterialTheme.typography.titleMedium)
|
||||||
|
if (canDeleteCash && isCash && !payment.id.isNullOrBlank()) {
|
||||||
|
IconButton(onClick = { onDelete(payment.id) }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Delete,
|
||||||
|
contentDescription = "Delete cash payment"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val methodColor = if (isCash) {
|
||||||
|
MaterialTheme.colorScheme.tertiary
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.secondary
|
||||||
|
}
|
||||||
|
val upiColor = MaterialTheme.colorScheme.primary
|
||||||
|
val payerColor = MaterialTheme.colorScheme.secondary
|
||||||
payment.method?.let {
|
payment.method?.let {
|
||||||
Text(text = "Method: $it", style = MaterialTheme.typography.bodySmall)
|
Text(
|
||||||
|
text = "Method: $it",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = methodColor
|
||||||
|
)
|
||||||
}
|
}
|
||||||
payment.payerVpa?.takeIf { it.isNotBlank() }?.let {
|
payment.payerVpa?.takeIf { it.isNotBlank() }?.let {
|
||||||
Text(text = "UPI: $it", style = MaterialTheme.typography.bodySmall)
|
Text(
|
||||||
|
text = "UPI: $it",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = upiColor
|
||||||
|
)
|
||||||
}
|
}
|
||||||
payment.payerName?.takeIf { it.isNotBlank() }?.let {
|
payment.payerName?.takeIf { it.isNotBlank() }?.let {
|
||||||
Text(text = "Payer: $it", style = MaterialTheme.typography.bodySmall)
|
Text(
|
||||||
|
text = "Payer: $it",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = payerColor
|
||||||
|
)
|
||||||
}
|
}
|
||||||
dateText?.let {
|
dateText?.let {
|
||||||
Text(text = "Received: $it", style = MaterialTheme.typography.bodySmall)
|
Text(text = "Received: $it", style = MaterialTheme.typography.bodySmall)
|
||||||
|
|||||||
@@ -87,4 +87,41 @@ class BookingPaymentsViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun deleteCashPayment(propertyId: String, bookingId: String, paymentId: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_state.update { it.copy(isLoading = true, error = null) }
|
||||||
|
try {
|
||||||
|
val api = ApiClient.create()
|
||||||
|
val response = api.deletePayment(
|
||||||
|
propertyId = propertyId,
|
||||||
|
bookingId = bookingId,
|
||||||
|
paymentId = paymentId
|
||||||
|
)
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
_state.update { current ->
|
||||||
|
current.copy(
|
||||||
|
isLoading = false,
|
||||||
|
payments = current.payments.filterNot { it.id == paymentId },
|
||||||
|
error = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
error = "Delete failed: ${response.code()}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
error = e.localizedMessage ?: "Delete failed"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
@@ -59,6 +60,10 @@ fun PayuQrScreen(
|
|||||||
val clipboard = LocalClipboardManager.current
|
val clipboard = LocalClipboardManager.current
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose { viewModel.reset() }
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(pendingAmount) {
|
LaunchedEffect(pendingAmount) {
|
||||||
viewModel.setInitialAmount(pendingAmount)
|
viewModel.setInitialAmount(pendingAmount)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ class PayuQrViewModel : ViewModel() {
|
|||||||
)
|
)
|
||||||
val state: StateFlow<PayuQrState> = _state
|
val state: StateFlow<PayuQrState> = _state
|
||||||
|
|
||||||
|
fun reset() {
|
||||||
|
_state.value = PayuQrState(deviceInfo = buildDeviceInfo())
|
||||||
|
}
|
||||||
|
|
||||||
fun onAmountChange(value: String) {
|
fun onAmountChange(value: String) {
|
||||||
val digits = value.filter { it.isDigit() }
|
val digits = value.filter { it.isDigit() }
|
||||||
_state.update { it.copy(amountInput = digits, error = null) }
|
_state.update { it.copy(amountInput = digits, error = null) }
|
||||||
|
|||||||
@@ -126,9 +126,7 @@ fun ActiveRoomStaysScreen(
|
|||||||
items(state.checkedInBookings) { booking ->
|
items(state.checkedInBookings) { booking ->
|
||||||
CheckedInBookingCard(
|
CheckedInBookingCard(
|
||||||
booking = booking,
|
booking = booking,
|
||||||
onClick = { selectedBooking.value = booking },
|
onClick = { onOpenBookingDetails(booking) })
|
||||||
onLongClick = { onOpenBookingDetails(booking) }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
@@ -152,12 +150,6 @@ fun ActiveRoomStaysScreen(
|
|||||||
) {
|
) {
|
||||||
Text("Manage room stay")
|
Text("Manage room stay")
|
||||||
}
|
}
|
||||||
TextButton(onClick = { selectedBooking.value = null }) {
|
|
||||||
Text("Balance")
|
|
||||||
}
|
|
||||||
TextButton(onClick = { selectedBooking.value = null }) {
|
|
||||||
Text("Add photos")
|
|
||||||
}
|
|
||||||
TextButton(onClick = { selectedBooking.value = null }) {
|
TextButton(onClick = { selectedBooking.value = null }) {
|
||||||
Text("Checkout")
|
Text("Checkout")
|
||||||
}
|
}
|
||||||
@@ -173,14 +165,11 @@ fun ActiveRoomStaysScreen(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun CheckedInBookingCard(
|
private fun CheckedInBookingCard(
|
||||||
booking: BookingListItem,
|
booking: BookingListItem,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit) {
|
||||||
onLongClick: () -> Unit
|
|
||||||
) {
|
|
||||||
Card(
|
Card(
|
||||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
|
||||||
modifier = Modifier.combinedClickable(
|
modifier = Modifier.combinedClickable(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = onLongClick
|
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(12.dp)) {
|
Column(modifier = Modifier.padding(12.dp)) {
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ import coil.request.ImageRequest
|
|||||||
import com.android.trisolarispms.data.api.ApiConstants
|
import com.android.trisolarispms.data.api.ApiConstants
|
||||||
import com.android.trisolarispms.data.api.FirebaseAuthTokenProvider
|
import com.android.trisolarispms.data.api.FirebaseAuthTokenProvider
|
||||||
import com.android.trisolarispms.data.api.model.BookingDetailsResponse
|
import com.android.trisolarispms.data.api.model.BookingDetailsResponse
|
||||||
|
import com.android.trisolarispms.ui.guestdocs.GuestDocumentsTab
|
||||||
import com.google.firebase.auth.FirebaseAuth
|
import com.google.firebase.auth.FirebaseAuth
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
@@ -74,10 +75,12 @@ fun BookingDetailsTabsScreen(
|
|||||||
onEditSignature: (String) -> Unit,
|
onEditSignature: (String) -> Unit,
|
||||||
onOpenPayuQr: (Long?, String?) -> Unit,
|
onOpenPayuQr: (Long?, String?) -> Unit,
|
||||||
onOpenPayments: () -> Unit,
|
onOpenPayments: () -> Unit,
|
||||||
|
canManageDocuments: Boolean,
|
||||||
staysViewModel: BookingRoomStaysViewModel = viewModel(),
|
staysViewModel: BookingRoomStaysViewModel = viewModel(),
|
||||||
detailsViewModel: BookingDetailsViewModel = viewModel()
|
detailsViewModel: BookingDetailsViewModel = viewModel()
|
||||||
) {
|
) {
|
||||||
val pagerState = rememberPagerState(initialPage = 0, pageCount = { 2 })
|
val tabCount = if (canManageDocuments) 3 else 2
|
||||||
|
val pagerState = rememberPagerState(initialPage = 0, pageCount = { tabCount })
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val staysState by staysViewModel.state.collectAsState()
|
val staysState by staysViewModel.state.collectAsState()
|
||||||
val detailsState by detailsViewModel.state.collectAsState()
|
val detailsState by detailsViewModel.state.collectAsState()
|
||||||
@@ -120,6 +123,15 @@ fun BookingDetailsTabsScreen(
|
|||||||
},
|
},
|
||||||
text = { Text("Room Stays") }
|
text = { Text("Room Stays") }
|
||||||
)
|
)
|
||||||
|
if (canManageDocuments) {
|
||||||
|
Tab(
|
||||||
|
selected = pagerState.currentPage == 2,
|
||||||
|
onClick = {
|
||||||
|
scope.launch { pagerState.animateScrollToPage(2) }
|
||||||
|
},
|
||||||
|
text = { Text("Manage Documents") }
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
state = pagerState,
|
state = pagerState,
|
||||||
@@ -138,6 +150,24 @@ fun BookingDetailsTabsScreen(
|
|||||||
onOpenPayments = onOpenPayments
|
onOpenPayments = onOpenPayments
|
||||||
)
|
)
|
||||||
1 -> BookingRoomStaysTabContent(staysState, staysViewModel)
|
1 -> BookingRoomStaysTabContent(staysState, staysViewModel)
|
||||||
|
2 -> if (canManageDocuments) {
|
||||||
|
val resolvedGuestId = detailsState.details?.guestId ?: guestId
|
||||||
|
if (!resolvedGuestId.isNullOrBlank()) {
|
||||||
|
GuestDocumentsTab(
|
||||||
|
propertyId = propertyId,
|
||||||
|
guestId = resolvedGuestId,
|
||||||
|
bookingId = bookingId,
|
||||||
|
canManageDocuments = canManageDocuments
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(text = "Guest not linked yet")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user