admins can delete cash payments

This commit is contained in:
androidlover5842
2026-01-30 11:40:49 +05:30
parent c5e0648dd1
commit 4fc080f146
7 changed files with 120 additions and 23 deletions

View File

@@ -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) {
@@ -486,6 +489,7 @@ class MainActivity : ComponentActivity() {
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,

View File

@@ -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>
} }

View File

@@ -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)) {
val amountText = buildString { Row(
append(payment.amount?.toString() ?: "-") modifier = Modifier.fillMaxWidth(),
payment.currency?.let { append(" $it") } horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
val amountText = buildString {
append(payment.amount?.toString() ?: "-")
payment.currency?.let { append(" $it") }
}
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"
)
}
}
} }
Text(text = amountText, style = MaterialTheme.typography.titleMedium) 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)

View File

@@ -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"
)
}
}
}
}
} }

View File

@@ -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)
} }

View File

@@ -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) }

View File

@@ -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)) {