add payment ledger impl and payu impl
This commit is contained in:
@@ -35,6 +35,7 @@ android {
|
|||||||
}
|
}
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose = true
|
compose = true
|
||||||
|
buildConfig = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,6 +61,7 @@ dependencies {
|
|||||||
implementation(libs.lottie.compose)
|
implementation(libs.lottie.compose)
|
||||||
implementation(libs.calendar.compose)
|
implementation(libs.calendar.compose)
|
||||||
implementation(libs.libphonenumber)
|
implementation(libs.libphonenumber)
|
||||||
|
implementation(libs.zxing.core)
|
||||||
implementation(platform(libs.firebase.bom))
|
implementation(platform(libs.firebase.bom))
|
||||||
implementation(libs.firebase.auth.ktx)
|
implementation(libs.firebase.auth.ktx)
|
||||||
implementation(libs.kotlinx.coroutines.play.services)
|
implementation(libs.kotlinx.coroutines.play.services)
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import com.android.trisolarispms.ui.roomstay.ManageRoomStaySelection
|
|||||||
import com.android.trisolarispms.ui.roomstay.BookingRoomStaysScreen
|
import com.android.trisolarispms.ui.roomstay.BookingRoomStaysScreen
|
||||||
import com.android.trisolarispms.ui.roomstay.BookingDetailsTabsScreen
|
import com.android.trisolarispms.ui.roomstay.BookingDetailsTabsScreen
|
||||||
import com.android.trisolarispms.ui.home.HomeScreen
|
import com.android.trisolarispms.ui.home.HomeScreen
|
||||||
|
import com.android.trisolarispms.ui.payment.BookingPaymentsScreen
|
||||||
import com.android.trisolarispms.ui.property.AddPropertyScreen
|
import com.android.trisolarispms.ui.property.AddPropertyScreen
|
||||||
import com.android.trisolarispms.ui.room.RoomFormScreen
|
import com.android.trisolarispms.ui.room.RoomFormScreen
|
||||||
import com.android.trisolarispms.ui.room.RoomsScreen
|
import com.android.trisolarispms.ui.room.RoomsScreen
|
||||||
@@ -35,6 +36,8 @@ import com.android.trisolarispms.ui.roomimage.ImageTagsScreen
|
|||||||
import com.android.trisolarispms.ui.roomimage.AddImageTagScreen
|
import com.android.trisolarispms.ui.roomimage.AddImageTagScreen
|
||||||
import com.android.trisolarispms.ui.roomimage.EditImageTagScreen
|
import com.android.trisolarispms.ui.roomimage.EditImageTagScreen
|
||||||
import com.android.trisolarispms.ui.roomstay.ActiveRoomStaysScreen
|
import com.android.trisolarispms.ui.roomstay.ActiveRoomStaysScreen
|
||||||
|
import com.android.trisolarispms.ui.payu.PayuSettingsScreen
|
||||||
|
import com.android.trisolarispms.ui.payu.PayuQrScreen
|
||||||
import com.android.trisolarispms.ui.roomtype.AddAmenityScreen
|
import com.android.trisolarispms.ui.roomtype.AddAmenityScreen
|
||||||
import com.android.trisolarispms.ui.roomtype.AddRoomTypeScreen
|
import com.android.trisolarispms.ui.roomtype.AddRoomTypeScreen
|
||||||
import com.android.trisolarispms.ui.roomtype.AmenitiesScreen
|
import com.android.trisolarispms.ui.roomtype.AmenitiesScreen
|
||||||
@@ -83,6 +86,11 @@ class MainActivity : ComponentActivity() {
|
|||||||
it == "ADMIN" || it == "MANAGER" || it == "STAFF"
|
it == "ADMIN" || it == "MANAGER" || it == "STAFF"
|
||||||
} == true
|
} == true
|
||||||
}
|
}
|
||||||
|
val canManagePayuSettings: (String) -> Boolean = { propertyId ->
|
||||||
|
state.isSuperAdmin || state.propertyRoles[propertyId]?.any {
|
||||||
|
it == "ADMIN" || it == "MANAGER"
|
||||||
|
} == true
|
||||||
|
}
|
||||||
|
|
||||||
BackHandler(enabled = currentRoute != AppRoute.Home) {
|
BackHandler(enabled = currentRoute != AppRoute.Home) {
|
||||||
when (currentRoute) {
|
when (currentRoute) {
|
||||||
@@ -114,6 +122,15 @@ class MainActivity : ComponentActivity() {
|
|||||||
currentRoute.propertyId,
|
currentRoute.propertyId,
|
||||||
currentRoute.roomTypeId
|
currentRoute.roomTypeId
|
||||||
)
|
)
|
||||||
|
is AppRoute.PayuSettings -> route.value = AppRoute.ActiveRoomStays(
|
||||||
|
currentRoute.propertyId,
|
||||||
|
selectedPropertyName.value ?: "Property"
|
||||||
|
)
|
||||||
|
is AppRoute.PayuQr -> route.value = AppRoute.BookingDetailsTabs(
|
||||||
|
currentRoute.propertyId,
|
||||||
|
currentRoute.bookingId,
|
||||||
|
null
|
||||||
|
)
|
||||||
is AppRoute.CreateBooking -> route.value = AppRoute.ActiveRoomStays(
|
is AppRoute.CreateBooking -> route.value = AppRoute.ActiveRoomStays(
|
||||||
currentRoute.propertyId,
|
currentRoute.propertyId,
|
||||||
selectedPropertyName.value ?: "Property"
|
selectedPropertyName.value ?: "Property"
|
||||||
@@ -157,6 +174,11 @@ class MainActivity : ComponentActivity() {
|
|||||||
currentRoute.propertyId,
|
currentRoute.propertyId,
|
||||||
selectedPropertyName.value ?: "Property"
|
selectedPropertyName.value ?: "Property"
|
||||||
)
|
)
|
||||||
|
is AppRoute.BookingPayments -> route.value = AppRoute.BookingDetailsTabs(
|
||||||
|
currentRoute.propertyId,
|
||||||
|
currentRoute.bookingId,
|
||||||
|
null
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,6 +275,8 @@ class MainActivity : ComponentActivity() {
|
|||||||
onBack = { route.value = AppRoute.Home },
|
onBack = { route.value = AppRoute.Home },
|
||||||
onViewRooms = { route.value = AppRoute.Rooms(currentRoute.propertyId) },
|
onViewRooms = { route.value = AppRoute.Rooms(currentRoute.propertyId) },
|
||||||
onCreateBooking = { route.value = AppRoute.CreateBooking(currentRoute.propertyId) },
|
onCreateBooking = { route.value = AppRoute.CreateBooking(currentRoute.propertyId) },
|
||||||
|
showPayuSettings = canManagePayuSettings(currentRoute.propertyId),
|
||||||
|
onPayuSettings = { route.value = AppRoute.PayuSettings(currentRoute.propertyId) },
|
||||||
onManageRoomStay = { booking ->
|
onManageRoomStay = { booking ->
|
||||||
val fromAt = booking.checkInAt?.takeIf { it.isNotBlank() }
|
val fromAt = booking.checkInAt?.takeIf { it.isNotBlank() }
|
||||||
?: booking.expectedCheckInAt.orEmpty()
|
?: booking.expectedCheckInAt.orEmpty()
|
||||||
@@ -281,6 +305,28 @@ class MainActivity : ComponentActivity() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
is AppRoute.PayuSettings -> PayuSettingsScreen(
|
||||||
|
propertyId = currentRoute.propertyId,
|
||||||
|
onBack = {
|
||||||
|
route.value = AppRoute.ActiveRoomStays(
|
||||||
|
currentRoute.propertyId,
|
||||||
|
selectedPropertyName.value ?: "Property"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
is AppRoute.PayuQr -> PayuQrScreen(
|
||||||
|
propertyId = currentRoute.propertyId,
|
||||||
|
bookingId = currentRoute.bookingId,
|
||||||
|
pendingAmount = currentRoute.pendingAmount,
|
||||||
|
guestPhone = currentRoute.guestPhone,
|
||||||
|
onBack = {
|
||||||
|
route.value = AppRoute.BookingDetailsTabs(
|
||||||
|
currentRoute.propertyId,
|
||||||
|
currentRoute.bookingId,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
is AppRoute.ManageRoomStaySelect -> ManageRoomStaySelectScreen(
|
is AppRoute.ManageRoomStaySelect -> ManageRoomStaySelectScreen(
|
||||||
propertyId = currentRoute.propertyId,
|
propertyId = currentRoute.propertyId,
|
||||||
bookingFromAt = currentRoute.fromAt,
|
bookingFromAt = currentRoute.fromAt,
|
||||||
@@ -420,6 +466,32 @@ class MainActivity : ComponentActivity() {
|
|||||||
currentRoute.bookingId,
|
currentRoute.bookingId,
|
||||||
guestId
|
guestId
|
||||||
)
|
)
|
||||||
|
},
|
||||||
|
onOpenPayuQr = { pendingAmount, guestPhone ->
|
||||||
|
route.value = AppRoute.PayuQr(
|
||||||
|
propertyId = currentRoute.propertyId,
|
||||||
|
bookingId = currentRoute.bookingId,
|
||||||
|
pendingAmount = pendingAmount,
|
||||||
|
guestPhone = guestPhone
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onOpenPayments = {
|
||||||
|
route.value = AppRoute.BookingPayments(
|
||||||
|
propertyId = currentRoute.propertyId,
|
||||||
|
bookingId = currentRoute.bookingId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
is AppRoute.BookingPayments -> BookingPaymentsScreen(
|
||||||
|
propertyId = currentRoute.propertyId,
|
||||||
|
bookingId = currentRoute.bookingId,
|
||||||
|
canAddCash = canManagePayuSettings(currentRoute.propertyId),
|
||||||
|
onBack = {
|
||||||
|
route.value = AppRoute.BookingDetailsTabs(
|
||||||
|
currentRoute.propertyId,
|
||||||
|
currentRoute.bookingId,
|
||||||
|
null
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
is AppRoute.Rooms -> RoomsScreen(
|
is AppRoute.Rooms -> RoomsScreen(
|
||||||
|
|||||||
@@ -15,4 +15,6 @@ interface ApiService :
|
|||||||
TransportApi,
|
TransportApi,
|
||||||
InboundEmailApi,
|
InboundEmailApi,
|
||||||
AmenityApi,
|
AmenityApi,
|
||||||
RatePlanApi
|
RatePlanApi,
|
||||||
|
PayuSettingsApi,
|
||||||
|
PayuPaymentLinkSettingsApi
|
||||||
|
|||||||
@@ -14,6 +14,12 @@ import com.android.trisolarispms.data.api.model.BookingBulkCheckInRequest
|
|||||||
import com.android.trisolarispms.data.api.model.BookingExpectedDatesRequest
|
import com.android.trisolarispms.data.api.model.BookingExpectedDatesRequest
|
||||||
import com.android.trisolarispms.data.api.model.BookingDetailsResponse
|
import com.android.trisolarispms.data.api.model.BookingDetailsResponse
|
||||||
import com.android.trisolarispms.data.api.model.RoomStayDto
|
import com.android.trisolarispms.data.api.model.RoomStayDto
|
||||||
|
import com.android.trisolarispms.data.api.model.PayuQrRequest
|
||||||
|
import com.android.trisolarispms.data.api.model.PayuQrResponse
|
||||||
|
import com.android.trisolarispms.data.api.model.PayuLinkRequest
|
||||||
|
import com.android.trisolarispms.data.api.model.PayuLinkResponse
|
||||||
|
import com.android.trisolarispms.data.api.model.PaymentDto
|
||||||
|
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
|
||||||
@@ -95,4 +101,31 @@ interface BookingApi {
|
|||||||
@Path("bookingId") bookingId: String,
|
@Path("bookingId") bookingId: String,
|
||||||
@Body body: BookingRoomStayCreateRequest
|
@Body body: BookingRoomStayCreateRequest
|
||||||
): Response<RoomStayDto>
|
): Response<RoomStayDto>
|
||||||
|
|
||||||
|
@POST("properties/{propertyId}/bookings/{bookingId}/payments/payu/qr")
|
||||||
|
suspend fun generatePayuQr(
|
||||||
|
@Path("propertyId") propertyId: String,
|
||||||
|
@Path("bookingId") bookingId: String,
|
||||||
|
@Body body: PayuQrRequest
|
||||||
|
): Response<PayuQrResponse>
|
||||||
|
|
||||||
|
@POST("properties/{propertyId}/bookings/{bookingId}/payments/payu/link")
|
||||||
|
suspend fun generatePayuLink(
|
||||||
|
@Path("propertyId") propertyId: String,
|
||||||
|
@Path("bookingId") bookingId: String,
|
||||||
|
@Body body: PayuLinkRequest
|
||||||
|
): Response<PayuLinkResponse>
|
||||||
|
|
||||||
|
@GET("properties/{propertyId}/bookings/{bookingId}/payments")
|
||||||
|
suspend fun listPayments(
|
||||||
|
@Path("propertyId") propertyId: String,
|
||||||
|
@Path("bookingId") bookingId: String
|
||||||
|
): Response<List<PaymentDto>>
|
||||||
|
|
||||||
|
@POST("properties/{propertyId}/bookings/{bookingId}/payments")
|
||||||
|
suspend fun createPayment(
|
||||||
|
@Path("propertyId") propertyId: String,
|
||||||
|
@Path("bookingId") bookingId: String,
|
||||||
|
@Body body: PaymentCreateRequest
|
||||||
|
): Response<PaymentDto>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.android.trisolarispms.data.api
|
||||||
|
|
||||||
|
import com.android.trisolarispms.data.api.model.PayuPaymentLinkSettingsRequest
|
||||||
|
import com.android.trisolarispms.data.api.model.PayuPaymentLinkSettingsResponse
|
||||||
|
import retrofit2.Response
|
||||||
|
import retrofit2.http.Body
|
||||||
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.PUT
|
||||||
|
import retrofit2.http.Path
|
||||||
|
|
||||||
|
interface PayuPaymentLinkSettingsApi {
|
||||||
|
@GET("properties/{propertyId}/payu-payment-link-settings")
|
||||||
|
suspend fun getPayuPaymentLinkSettings(
|
||||||
|
@Path("propertyId") propertyId: String
|
||||||
|
): Response<PayuPaymentLinkSettingsResponse>
|
||||||
|
|
||||||
|
@PUT("properties/{propertyId}/payu-payment-link-settings")
|
||||||
|
suspend fun updatePayuPaymentLinkSettings(
|
||||||
|
@Path("propertyId") propertyId: String,
|
||||||
|
@Body body: PayuPaymentLinkSettingsRequest
|
||||||
|
): Response<PayuPaymentLinkSettingsResponse>
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.android.trisolarispms.data.api
|
||||||
|
|
||||||
|
import com.android.trisolarispms.data.api.model.PayuSettingsRequest
|
||||||
|
import com.android.trisolarispms.data.api.model.PayuSettingsResponse
|
||||||
|
import retrofit2.Response
|
||||||
|
import retrofit2.http.Body
|
||||||
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.PUT
|
||||||
|
import retrofit2.http.Path
|
||||||
|
|
||||||
|
interface PayuSettingsApi {
|
||||||
|
@GET("properties/{propertyId}/payu-settings")
|
||||||
|
suspend fun getPayuSettings(
|
||||||
|
@Path("propertyId") propertyId: String
|
||||||
|
): Response<PayuSettingsResponse>
|
||||||
|
|
||||||
|
@PUT("properties/{propertyId}/payu-settings")
|
||||||
|
suspend fun updatePayuSettings(
|
||||||
|
@Path("propertyId") propertyId: String,
|
||||||
|
@Body body: PayuSettingsRequest
|
||||||
|
): Response<PayuSettingsResponse>
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.android.trisolarispms.data.api.model
|
||||||
|
|
||||||
|
data class PaymentDto(
|
||||||
|
val id: String? = null,
|
||||||
|
val bookingId: String? = null,
|
||||||
|
val amount: Long? = null,
|
||||||
|
val currency: String? = null,
|
||||||
|
val method: String? = null,
|
||||||
|
val gatewayPaymentId: String? = null,
|
||||||
|
val gatewayTxnId: String? = null,
|
||||||
|
val bankRefNum: String? = null,
|
||||||
|
val mode: String? = null,
|
||||||
|
val pgType: String? = null,
|
||||||
|
val payerVpa: String? = null,
|
||||||
|
val payerName: String? = null,
|
||||||
|
val paymentSource: String? = null,
|
||||||
|
val reference: String? = null,
|
||||||
|
val notes: String? = null,
|
||||||
|
val receivedAt: String? = null,
|
||||||
|
val receivedByUserId: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class PaymentCreateRequest(
|
||||||
|
val amount: Long
|
||||||
|
)
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.android.trisolarispms.data.api.model
|
||||||
|
|
||||||
|
data class PayuPaymentLinkSettingsRequest(
|
||||||
|
val merchantId: String,
|
||||||
|
val clientId: String,
|
||||||
|
val clientSecret: String,
|
||||||
|
val isTest: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
data class PayuPaymentLinkSettingsResponse(
|
||||||
|
val propertyId: String? = null,
|
||||||
|
val configured: Boolean? = null,
|
||||||
|
val merchantId: String? = null,
|
||||||
|
val isTest: Boolean? = null,
|
||||||
|
val hasClientId: Boolean? = null,
|
||||||
|
val hasClientSecret: Boolean? = null,
|
||||||
|
val hasAccessToken: Boolean? = null
|
||||||
|
)
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.android.trisolarispms.data.api.model
|
||||||
|
|
||||||
|
import com.google.gson.JsonElement
|
||||||
|
|
||||||
|
data class PayuQrRequest(
|
||||||
|
val amount: Long,
|
||||||
|
val deviceInfo: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class PayuQrResponse(
|
||||||
|
val txnid: String? = null,
|
||||||
|
val amount: Long? = null,
|
||||||
|
val currency: String? = null,
|
||||||
|
val payuResponse: JsonElement? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class PayuLinkRequest(
|
||||||
|
val amount: Long
|
||||||
|
)
|
||||||
|
|
||||||
|
data class PayuLinkResponse(
|
||||||
|
val amount: Long? = null,
|
||||||
|
val currency: String? = null,
|
||||||
|
val paymentLink: String? = null,
|
||||||
|
val payuResponse: JsonElement? = null
|
||||||
|
)
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.android.trisolarispms.data.api.model
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
data class PayuSettingsRequest(
|
||||||
|
val merchantKey: String,
|
||||||
|
val salt32: String? = null,
|
||||||
|
val salt256: String? = null,
|
||||||
|
@SerializedName("isTest") val isTest: Boolean,
|
||||||
|
val useSalt256: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
data class PayuSettingsResponse(
|
||||||
|
val propertyId: String? = null,
|
||||||
|
val configured: Boolean? = null,
|
||||||
|
val merchantKey: String? = null,
|
||||||
|
@SerializedName("test") val isTest: Boolean? = null,
|
||||||
|
val useSalt256: Boolean? = null,
|
||||||
|
val hasSalt32: Boolean? = null,
|
||||||
|
val hasSalt256: Boolean? = null
|
||||||
|
)
|
||||||
@@ -47,6 +47,10 @@ sealed interface AppRoute {
|
|||||||
val bookingId: String,
|
val bookingId: String,
|
||||||
val guestId: String?
|
val guestId: String?
|
||||||
) : AppRoute
|
) : AppRoute
|
||||||
|
data class BookingPayments(
|
||||||
|
val propertyId: String,
|
||||||
|
val bookingId: String
|
||||||
|
) : AppRoute
|
||||||
data object AddProperty : AppRoute
|
data object AddProperty : AppRoute
|
||||||
data class ActiveRoomStays(val propertyId: String, val propertyName: String) : AppRoute
|
data class ActiveRoomStays(val propertyId: String, val propertyName: String) : AppRoute
|
||||||
data class Rooms(val propertyId: String) : AppRoute
|
data class Rooms(val propertyId: String) : AppRoute
|
||||||
@@ -63,6 +67,13 @@ sealed interface AppRoute {
|
|||||||
val ratePlanId: String,
|
val ratePlanId: String,
|
||||||
val ratePlanCode: String
|
val ratePlanCode: String
|
||||||
) : AppRoute
|
) : AppRoute
|
||||||
|
data class PayuSettings(val propertyId: String) : AppRoute
|
||||||
|
data class PayuQr(
|
||||||
|
val propertyId: String,
|
||||||
|
val bookingId: String,
|
||||||
|
val pendingAmount: Long?,
|
||||||
|
val guestPhone: String?
|
||||||
|
) : AppRoute
|
||||||
data object Amenities : AppRoute
|
data object Amenities : AppRoute
|
||||||
data object AddAmenity : AppRoute
|
data object AddAmenity : AppRoute
|
||||||
data class EditAmenity(val amenityId: String) : AppRoute
|
data class EditAmenity(val amenityId: String) : AppRoute
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
package com.android.trisolarispms.ui.payment
|
||||||
|
|
||||||
|
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.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
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
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
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.PaymentDto
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
fun BookingPaymentsScreen(
|
||||||
|
propertyId: String,
|
||||||
|
bookingId: String,
|
||||||
|
canAddCash: Boolean,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
viewModel: BookingPaymentsViewModel = viewModel()
|
||||||
|
) {
|
||||||
|
val state by viewModel.state.collectAsState()
|
||||||
|
val amountInput = remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
LaunchedEffect(propertyId, bookingId) {
|
||||||
|
viewModel.load(propertyId, bookingId)
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text("Payments") },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onBack) {
|
||||||
|
Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = "Back")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { padding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(padding)
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
if (canAddCash) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = amountInput.value,
|
||||||
|
onValueChange = { amountInput.value = it.filter { ch -> ch.isDigit() } },
|
||||||
|
label = { Text("Cash amount") },
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
val amount = amountInput.value.toLongOrNull() ?: 0L
|
||||||
|
viewModel.addCashPayment(propertyId, bookingId, amount)
|
||||||
|
},
|
||||||
|
enabled = !state.isLoading,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text("Add cash payment")
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(12.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))
|
||||||
|
}
|
||||||
|
if (!state.isLoading && state.error == null && state.payments.isEmpty()) {
|
||||||
|
Text(text = "No payments yet")
|
||||||
|
}
|
||||||
|
LazyColumn(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
items(state.payments) { payment ->
|
||||||
|
PaymentCard(payment = payment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PaymentCard(payment: PaymentDto) {
|
||||||
|
val date = payment.receivedAt?.let {
|
||||||
|
runCatching { OffsetDateTime.parse(it) }.getOrNull()
|
||||||
|
}
|
||||||
|
val dateText = date?.format(DateTimeFormatter.ofPattern("dd MMM yyyy, hh:mm a"))
|
||||||
|
val isCash = payment.method == "CASH"
|
||||||
|
Card(
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = if (isCash) {
|
||||||
|
MaterialTheme.colorScheme.tertiaryContainer
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
}
|
||||||
|
),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(12.dp)) {
|
||||||
|
val amountText = buildString {
|
||||||
|
append(payment.amount?.toString() ?: "-")
|
||||||
|
payment.currency?.let { append(" $it") }
|
||||||
|
}
|
||||||
|
Text(text = amountText, style = MaterialTheme.typography.titleMedium)
|
||||||
|
payment.method?.let {
|
||||||
|
Text(text = "Method: $it", style = MaterialTheme.typography.bodySmall)
|
||||||
|
}
|
||||||
|
payment.payerVpa?.takeIf { it.isNotBlank() }?.let {
|
||||||
|
Text(text = "UPI: $it", style = MaterialTheme.typography.bodySmall)
|
||||||
|
}
|
||||||
|
payment.payerName?.takeIf { it.isNotBlank() }?.let {
|
||||||
|
Text(text = "Payer: $it", style = MaterialTheme.typography.bodySmall)
|
||||||
|
}
|
||||||
|
dateText?.let {
|
||||||
|
Text(text = "Received: $it", style = MaterialTheme.typography.bodySmall)
|
||||||
|
}
|
||||||
|
payment.reference?.takeIf { it.isNotBlank() }?.let {
|
||||||
|
Text(text = "Ref: $it", style = MaterialTheme.typography.bodySmall)
|
||||||
|
}
|
||||||
|
payment.notes?.takeIf { it.isNotBlank() }?.let {
|
||||||
|
Text(text = it, style = MaterialTheme.typography.bodySmall)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.android.trisolarispms.ui.payment
|
||||||
|
|
||||||
|
import com.android.trisolarispms.data.api.model.PaymentDto
|
||||||
|
|
||||||
|
data class BookingPaymentsState(
|
||||||
|
val isLoading: Boolean = false,
|
||||||
|
val error: String? = null,
|
||||||
|
val payments: List<PaymentDto> = emptyList()
|
||||||
|
)
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package com.android.trisolarispms.ui.payment
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.android.trisolarispms.data.api.ApiClient
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class BookingPaymentsViewModel : ViewModel() {
|
||||||
|
private val _state = MutableStateFlow(BookingPaymentsState())
|
||||||
|
val state: StateFlow<BookingPaymentsState> = _state
|
||||||
|
|
||||||
|
fun load(propertyId: String, bookingId: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_state.update { it.copy(isLoading = true, error = null) }
|
||||||
|
try {
|
||||||
|
val api = ApiClient.create()
|
||||||
|
val response = api.listPayments(propertyId, bookingId)
|
||||||
|
val body = response.body()
|
||||||
|
if (response.isSuccessful && body != null) {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
payments = body,
|
||||||
|
error = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} 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 addCashPayment(propertyId: String, bookingId: String, amount: Long) {
|
||||||
|
if (amount <= 0) {
|
||||||
|
_state.update { it.copy(error = "Amount must be greater than 0") }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
viewModelScope.launch {
|
||||||
|
_state.update { it.copy(isLoading = true, error = null) }
|
||||||
|
try {
|
||||||
|
val api = ApiClient.create()
|
||||||
|
val response = api.createPayment(
|
||||||
|
propertyId = propertyId,
|
||||||
|
bookingId = bookingId,
|
||||||
|
body = com.android.trisolarispms.data.api.model.PaymentCreateRequest(amount = amount)
|
||||||
|
)
|
||||||
|
val body = response.body()
|
||||||
|
if (response.isSuccessful && body != null) {
|
||||||
|
_state.update { current ->
|
||||||
|
current.copy(
|
||||||
|
isLoading = false,
|
||||||
|
payments = listOf(body) + current.payments,
|
||||||
|
error = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
error = "Create failed: ${response.code()}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
error = e.localizedMessage ?: "Create failed"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.android.trisolarispms.ui.payu
|
||||||
|
|
||||||
|
data class PayuPaymentLinkSettingsState(
|
||||||
|
val merchantId: String = "",
|
||||||
|
val clientId: String = "",
|
||||||
|
val clientSecret: String = "",
|
||||||
|
val isTest: Boolean = false,
|
||||||
|
val configured: Boolean = false,
|
||||||
|
val hasClientId: Boolean = false,
|
||||||
|
val hasClientSecret: Boolean = false,
|
||||||
|
val hasAccessToken: Boolean = false,
|
||||||
|
val isLoading: Boolean = false,
|
||||||
|
val isSaving: Boolean = false,
|
||||||
|
val error: String? = null,
|
||||||
|
val message: String? = null
|
||||||
|
)
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
package com.android.trisolarispms.ui.payu
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.android.trisolarispms.data.api.ApiClient
|
||||||
|
import com.android.trisolarispms.data.api.model.PayuPaymentLinkSettingsRequest
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class PayuPaymentLinkSettingsViewModel : ViewModel() {
|
||||||
|
private val _state = MutableStateFlow(PayuPaymentLinkSettingsState())
|
||||||
|
val state: StateFlow<PayuPaymentLinkSettingsState> = _state
|
||||||
|
|
||||||
|
fun load(propertyId: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_state.update { it.copy(isLoading = true, error = null, message = null) }
|
||||||
|
try {
|
||||||
|
val api = ApiClient.create()
|
||||||
|
val response = api.getPayuPaymentLinkSettings(propertyId)
|
||||||
|
val body = response.body()
|
||||||
|
if (response.isSuccessful && body != null) {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
merchantId = body.merchantId.orEmpty(),
|
||||||
|
isTest = body.isTest == true,
|
||||||
|
configured = body.configured == true,
|
||||||
|
hasClientId = body.hasClientId == true,
|
||||||
|
hasClientSecret = body.hasClientSecret == true,
|
||||||
|
hasAccessToken = body.hasAccessToken == true,
|
||||||
|
clientId = "",
|
||||||
|
clientSecret = "",
|
||||||
|
isLoading = false,
|
||||||
|
error = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
error = "Load failed: ${response.code()}",
|
||||||
|
message = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
error = e.localizedMessage ?: "Load failed",
|
||||||
|
message = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onMerchantIdChange(value: String) {
|
||||||
|
_state.update { it.copy(merchantId = value, error = null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onClientIdChange(value: String) {
|
||||||
|
_state.update { it.copy(clientId = value, error = null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onClientSecretChange(value: String) {
|
||||||
|
_state.update { it.copy(clientSecret = value, error = null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onIsTestChange(value: Boolean) {
|
||||||
|
_state.update { it.copy(isTest = value, error = null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun save(propertyId: String) {
|
||||||
|
val current = state.value
|
||||||
|
val merchantId = current.merchantId.trim()
|
||||||
|
val clientId = current.clientId.trim()
|
||||||
|
val clientSecret = current.clientSecret.trim()
|
||||||
|
if (merchantId.isBlank() || clientId.isBlank() || clientSecret.isBlank()) {
|
||||||
|
_state.update { it.copy(error = "All fields are required") }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
viewModelScope.launch {
|
||||||
|
_state.update { it.copy(isSaving = true, error = null, message = null) }
|
||||||
|
try {
|
||||||
|
val api = ApiClient.create()
|
||||||
|
val response = api.updatePayuPaymentLinkSettings(
|
||||||
|
propertyId = propertyId,
|
||||||
|
body = PayuPaymentLinkSettingsRequest(
|
||||||
|
merchantId = merchantId,
|
||||||
|
clientId = clientId,
|
||||||
|
clientSecret = clientSecret,
|
||||||
|
isTest = current.isTest
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
_state.update { it.copy(isSaving = false, message = "Saved", error = null) }
|
||||||
|
load(propertyId)
|
||||||
|
} else {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isSaving = false,
|
||||||
|
error = "Save failed: ${response.code()}",
|
||||||
|
message = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isSaving = false,
|
||||||
|
error = e.localizedMessage ?: "Save failed",
|
||||||
|
message = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
package com.android.trisolarispms.ui.payu
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
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.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.google.zxing.BarcodeFormat
|
||||||
|
import com.google.zxing.qrcode.QRCodeWriter
|
||||||
|
import com.google.zxing.common.BitMatrix
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
fun PayuQrScreen(
|
||||||
|
propertyId: String,
|
||||||
|
bookingId: String,
|
||||||
|
pendingAmount: Long?,
|
||||||
|
guestPhone: String?,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
viewModel: PayuQrViewModel = viewModel()
|
||||||
|
) {
|
||||||
|
val state by viewModel.state.collectAsState()
|
||||||
|
val clipboard = LocalClipboardManager.current
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
LaunchedEffect(pendingAmount) {
|
||||||
|
viewModel.setInitialAmount(pendingAmount)
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text("PayU QR") },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onBack) {
|
||||||
|
Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = "Back")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { padding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(padding)
|
||||||
|
.padding(24.dp)
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
|
verticalArrangement = Arrangement.Top
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state.amountInput,
|
||||||
|
onValueChange = viewModel::onAmountChange,
|
||||||
|
label = { Text("Amount") },
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
if (!guestPhone.isNullOrBlank()) {
|
||||||
|
viewModel.generateLink(propertyId, bookingId) { link ->
|
||||||
|
val uri = Uri.parse("smsto:${guestPhone}")
|
||||||
|
val intent = Intent(Intent.ACTION_SENDTO, uri)
|
||||||
|
.putExtra("sms_body", "Pay using this link: $link")
|
||||||
|
context.startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled = !state.isLoading && !guestPhone.isNullOrBlank(),
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Text("Share Link")
|
||||||
|
}
|
||||||
|
Button(
|
||||||
|
onClick = { viewModel.generate(propertyId, bookingId) },
|
||||||
|
enabled = !state.isLoading,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
if (state.isLoading) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(16.dp)
|
||||||
|
.padding(end = 8.dp),
|
||||||
|
strokeWidth = 2.dp
|
||||||
|
)
|
||||||
|
Text("Generating...")
|
||||||
|
} else {
|
||||||
|
Text("Generate QR")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.error?.let {
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
Text(text = it, color = MaterialTheme.colorScheme.error)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!state.qrString.isNullOrBlank()) {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
val qrBitmap = remember(state.qrString) { state.qrString?.let { createQrBitmap(it) } }
|
||||||
|
qrBitmap?.let {
|
||||||
|
androidx.compose.foundation.Image(
|
||||||
|
bitmap = it.asImageBitmap(),
|
||||||
|
contentDescription = "PayU QR",
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(240.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Amount: ${state.amount ?: "-"} ${state.currency ?: ""}",
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
clipboard.setText(AnnotatedString(state.qrString.orEmpty()))
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text("Copy QR string")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createQrBitmap(content: String, size: Int = 600): Bitmap {
|
||||||
|
val matrix: BitMatrix = QRCodeWriter().encode(content, BarcodeFormat.QR_CODE, size, size)
|
||||||
|
val width = matrix.width
|
||||||
|
val height = matrix.height
|
||||||
|
val pixels = IntArray(width * height)
|
||||||
|
for (y in 0 until height) {
|
||||||
|
val offset = y * width
|
||||||
|
for (x in 0 until width) {
|
||||||
|
pixels[offset + x] = if (matrix.get(x, y)) 0xFF000000.toInt() else 0xFFFFFFFF.toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888).apply {
|
||||||
|
setPixels(pixels, 0, width, 0, 0, width, height)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.android.trisolarispms.ui.payu
|
||||||
|
|
||||||
|
data class PayuQrState(
|
||||||
|
val deviceInfo: String = "",
|
||||||
|
val amountInput: String = "",
|
||||||
|
val isLoading: Boolean = false,
|
||||||
|
val error: String? = null,
|
||||||
|
val txnId: String? = null,
|
||||||
|
val amount: Long? = null,
|
||||||
|
val currency: String? = null,
|
||||||
|
val qrString: String? = null,
|
||||||
|
val payuResponseRaw: String? = null,
|
||||||
|
val paymentLink: String? = null
|
||||||
|
)
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
package com.android.trisolarispms.ui.payu
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.android.trisolarispms.data.api.ApiClient
|
||||||
|
import com.android.trisolarispms.data.api.model.PayuLinkRequest
|
||||||
|
import com.android.trisolarispms.data.api.model.PayuQrRequest
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.JsonElement
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class PayuQrViewModel : ViewModel() {
|
||||||
|
private val gson = Gson()
|
||||||
|
private val _state = MutableStateFlow(
|
||||||
|
PayuQrState(deviceInfo = buildDeviceInfo())
|
||||||
|
)
|
||||||
|
val state: StateFlow<PayuQrState> = _state
|
||||||
|
|
||||||
|
fun onAmountChange(value: String) {
|
||||||
|
val digits = value.filter { it.isDigit() }
|
||||||
|
_state.update { it.copy(amountInput = digits, error = null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setInitialAmount(amount: Long?) {
|
||||||
|
if (amount == null || amount <= 0) return
|
||||||
|
_state.update { current ->
|
||||||
|
if (current.amountInput.isBlank()) {
|
||||||
|
current.copy(amountInput = amount.toString())
|
||||||
|
} else {
|
||||||
|
current
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generate(propertyId: String, bookingId: String) {
|
||||||
|
val current = state.value
|
||||||
|
val amount = current.amountInput.toLongOrNull()?.takeIf { it > 0 }
|
||||||
|
if (amount == null) {
|
||||||
|
_state.update { it.copy(error = "Amount is required") }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
viewModelScope.launch {
|
||||||
|
_state.update { it.copy(isLoading = true, error = null) }
|
||||||
|
try {
|
||||||
|
val api = ApiClient.create()
|
||||||
|
val response = api.generatePayuQr(
|
||||||
|
propertyId = propertyId,
|
||||||
|
bookingId = bookingId,
|
||||||
|
body = PayuQrRequest(
|
||||||
|
amount = amount,
|
||||||
|
deviceInfo = current.deviceInfo
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val body = response.body()
|
||||||
|
if (response.isSuccessful && body != null) {
|
||||||
|
val (qrString, rawPayload) = extractQrString(body.payuResponse)
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
txnId = body.txnid,
|
||||||
|
amount = body.amount,
|
||||||
|
currency = body.currency,
|
||||||
|
qrString = qrString,
|
||||||
|
payuResponseRaw = rawPayload,
|
||||||
|
error = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
error = "QR request failed: ${response.code()}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
error = e.localizedMessage ?: "QR request failed"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generateLink(propertyId: String, bookingId: String, onReady: (String) -> Unit) {
|
||||||
|
val current = state.value
|
||||||
|
val amount = current.amountInput.toLongOrNull()?.takeIf { it > 0 }
|
||||||
|
if (amount == null) {
|
||||||
|
_state.update { it.copy(error = "Amount is required") }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
viewModelScope.launch {
|
||||||
|
_state.update { it.copy(isLoading = true, error = null) }
|
||||||
|
try {
|
||||||
|
val api = ApiClient.create()
|
||||||
|
val response = api.generatePayuLink(
|
||||||
|
propertyId = propertyId,
|
||||||
|
bookingId = bookingId,
|
||||||
|
body = PayuLinkRequest(amount = amount)
|
||||||
|
)
|
||||||
|
val body = response.body()
|
||||||
|
if (response.isSuccessful && body?.paymentLink != null) {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
paymentLink = body.paymentLink,
|
||||||
|
amount = body.amount,
|
||||||
|
currency = body.currency,
|
||||||
|
error = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onReady(body.paymentLink)
|
||||||
|
} else {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
error = "Link request failed: ${response.code()}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
error = e.localizedMessage ?: "Link request failed"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun extractQrString(payuResponse: JsonElement?): Pair<String?, String?> {
|
||||||
|
if (payuResponse == null) return null to null
|
||||||
|
val raw = gson.toJson(payuResponse)
|
||||||
|
val root = when {
|
||||||
|
payuResponse.isJsonObject -> payuResponse.asJsonObject
|
||||||
|
payuResponse.isJsonPrimitive && payuResponse.asJsonPrimitive.isString -> {
|
||||||
|
runCatching {
|
||||||
|
gson.fromJson(payuResponse.asString, JsonObject::class.java)
|
||||||
|
}.getOrNull()
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
val qrString = root?.getAsJsonObject("result")?.get("qrString")?.asString
|
||||||
|
return qrString to raw
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildDeviceInfo(): String {
|
||||||
|
val release = android.os.Build.VERSION.RELEASE
|
||||||
|
val model = android.os.Build.MODEL
|
||||||
|
return "Android $release; $model; PMS"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,437 @@
|
|||||||
|
package com.android.trisolarispms.ui.payu
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
|
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.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.Switch
|
||||||
|
import androidx.compose.material3.Tab
|
||||||
|
import androidx.compose.material3.TabRow
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.platform.ClipboardManager
|
||||||
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
fun PayuSettingsScreen(
|
||||||
|
propertyId: String,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
viewModel: PayuSettingsViewModel = viewModel(),
|
||||||
|
linkViewModel: PayuPaymentLinkSettingsViewModel = viewModel()
|
||||||
|
) {
|
||||||
|
val state by viewModel.state.collectAsState()
|
||||||
|
val linkState by linkViewModel.state.collectAsState()
|
||||||
|
val clipboard = LocalClipboardManager.current
|
||||||
|
val pagerState = rememberPagerState(initialPage = 0, pageCount = { 2 })
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
LaunchedEffect(propertyId) {
|
||||||
|
viewModel.load(propertyId)
|
||||||
|
linkViewModel.load(propertyId)
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text("Payu Settings") },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onBack) {
|
||||||
|
Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = "Back")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { padding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(padding)
|
||||||
|
) {
|
||||||
|
TabRow(selectedTabIndex = pagerState.currentPage) {
|
||||||
|
Tab(
|
||||||
|
selected = pagerState.currentPage == 0,
|
||||||
|
onClick = { scope.launch { pagerState.animateScrollToPage(0) } },
|
||||||
|
text = { Text("QR") }
|
||||||
|
)
|
||||||
|
Tab(
|
||||||
|
selected = pagerState.currentPage == 1,
|
||||||
|
onClick = { scope.launch { pagerState.animateScrollToPage(1) } },
|
||||||
|
text = { Text("Payment Links") }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
HorizontalPager(
|
||||||
|
state = pagerState,
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) { page ->
|
||||||
|
when (page) {
|
||||||
|
0 -> PayuQrSettingsTab(
|
||||||
|
propertyId = propertyId,
|
||||||
|
state = state,
|
||||||
|
onMerchantKeyChange = viewModel::onMerchantKeyChange,
|
||||||
|
onIsTestChange = viewModel::onIsTestChange,
|
||||||
|
onUseSalt256Change = viewModel::onUseSalt256Change,
|
||||||
|
onSalt32Change = viewModel::onSalt32Change,
|
||||||
|
onSalt256Change = viewModel::onSalt256Change,
|
||||||
|
onSave = { viewModel.save(propertyId) },
|
||||||
|
clipboard = clipboard
|
||||||
|
)
|
||||||
|
1 -> PayuPaymentLinksTab(
|
||||||
|
propertyId = propertyId,
|
||||||
|
state = linkState,
|
||||||
|
onMerchantIdChange = linkViewModel::onMerchantIdChange,
|
||||||
|
onClientIdChange = linkViewModel::onClientIdChange,
|
||||||
|
onClientSecretChange = linkViewModel::onClientSecretChange,
|
||||||
|
onIsTestChange = linkViewModel::onIsTestChange,
|
||||||
|
onSave = { linkViewModel.save(propertyId) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PayuQrSettingsTab(
|
||||||
|
propertyId: String,
|
||||||
|
state: PayuSettingsState,
|
||||||
|
onMerchantKeyChange: (String) -> Unit,
|
||||||
|
onIsTestChange: (Boolean) -> Unit,
|
||||||
|
onUseSalt256Change: (Boolean) -> Unit,
|
||||||
|
onSalt32Change: (String) -> Unit,
|
||||||
|
onSalt256Change: (String) -> Unit,
|
||||||
|
onSave: () -> Unit,
|
||||||
|
clipboard: ClipboardManager
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(24.dp)
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
|
verticalArrangement = Arrangement.Top
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Payu Settings",
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
text = "More payment settings will be added later. For now, configure PayU.",
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
text = "Success/failure URLs are derived by the server using this property ID.",
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
if (state.isLoading) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = if (state.configured) "Status: Configured" else "Status: Not configured",
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
state.message?.let {
|
||||||
|
Text(text = it, color = MaterialTheme.colorScheme.primary)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
state.error?.let {
|
||||||
|
Text(text = it, color = MaterialTheme.colorScheme.error)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state.merchantKey,
|
||||||
|
onValueChange = onMerchantKeyChange,
|
||||||
|
label = { Text("Merchant key") },
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(text = "Use PayU test URL")
|
||||||
|
Switch(
|
||||||
|
checked = state.isTest,
|
||||||
|
onCheckedChange = onIsTestChange
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
|
Text(
|
||||||
|
text = if (state.isTest) {
|
||||||
|
"Base URL: https://test.payu.in/_payment"
|
||||||
|
} else {
|
||||||
|
"Base URL: https://secure.payu.in/_payment"
|
||||||
|
},
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(text = "Use salt256")
|
||||||
|
Switch(
|
||||||
|
checked = state.useSalt256,
|
||||||
|
onCheckedChange = onUseSalt256Change
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
if (state.useSalt256) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state.salt256,
|
||||||
|
onValueChange = onSalt256Change,
|
||||||
|
label = { Text("Salt256") },
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
if (state.hasSalt256) {
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
text = "Salt256 saved",
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state.salt32,
|
||||||
|
onValueChange = onSalt32Change,
|
||||||
|
label = { Text("Salt32") },
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
if (state.hasSalt32) {
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
text = "Salt32 saved",
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = onSave,
|
||||||
|
enabled = !state.isSaving,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
if (state.isSaving) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(16.dp)
|
||||||
|
.padding(end = 8.dp),
|
||||||
|
strokeWidth = 2.dp
|
||||||
|
)
|
||||||
|
Text("Saving...")
|
||||||
|
} else {
|
||||||
|
Text("Save")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = "https://api.hoteltrisolaris.in/properties/$propertyId/payu/return/success",
|
||||||
|
onValueChange = {},
|
||||||
|
label = { Text("Success URL") },
|
||||||
|
readOnly = true,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = "https://api.hoteltrisolaris.in/properties/$propertyId/payu/return/failure",
|
||||||
|
onValueChange = {},
|
||||||
|
label = { Text("Failure URL") },
|
||||||
|
readOnly = true,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
clipboard.setText(
|
||||||
|
AnnotatedString(
|
||||||
|
"https://api.hoteltrisolaris.in/properties/$propertyId/payu/return/success"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text("Copy success URL")
|
||||||
|
}
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
clipboard.setText(
|
||||||
|
AnnotatedString(
|
||||||
|
"https://api.hoteltrisolaris.in/properties/$propertyId/payu/return/failure"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text("Copy failure URL")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PayuPaymentLinksTab(
|
||||||
|
propertyId: String,
|
||||||
|
state: PayuPaymentLinkSettingsState,
|
||||||
|
onMerchantIdChange: (String) -> Unit,
|
||||||
|
onClientIdChange: (String) -> Unit,
|
||||||
|
onClientSecretChange: (String) -> Unit,
|
||||||
|
onIsTestChange: (Boolean) -> Unit,
|
||||||
|
onSave: () -> Unit
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(24.dp)
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
|
verticalArrangement = Arrangement.Top
|
||||||
|
) {
|
||||||
|
if (state.isLoading) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = if (state.configured) "Status: Configured" else "Status: Not configured",
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
if (state.hasClientId) {
|
||||||
|
Text(text = "Client ID saved", style = MaterialTheme.typography.bodySmall)
|
||||||
|
}
|
||||||
|
if (state.hasClientSecret) {
|
||||||
|
Text(text = "Client Secret saved", style = MaterialTheme.typography.bodySmall)
|
||||||
|
}
|
||||||
|
if (state.hasAccessToken) {
|
||||||
|
Text(text = "Access token available", style = MaterialTheme.typography.bodySmall)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
Text(
|
||||||
|
text = "Payment Link Settings",
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
state.message?.let {
|
||||||
|
Text(text = it, color = MaterialTheme.colorScheme.primary)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
state.error?.let {
|
||||||
|
Text(text = it, color = MaterialTheme.colorScheme.error)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state.merchantId,
|
||||||
|
onValueChange = onMerchantIdChange,
|
||||||
|
label = { Text("Merchant ID") },
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state.clientId,
|
||||||
|
onValueChange = onClientIdChange,
|
||||||
|
label = { Text("Client ID") },
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state.clientSecret,
|
||||||
|
onValueChange = onClientSecretChange,
|
||||||
|
label = { Text("Client Secret") },
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(text = "Use PayU test environment")
|
||||||
|
Switch(
|
||||||
|
checked = state.isTest,
|
||||||
|
onCheckedChange = onIsTestChange
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = onSave,
|
||||||
|
enabled = !state.isSaving,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
if (state.isSaving) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(16.dp)
|
||||||
|
.padding(end = 8.dp),
|
||||||
|
strokeWidth = 2.dp
|
||||||
|
)
|
||||||
|
Text("Saving...")
|
||||||
|
} else {
|
||||||
|
Text("Save")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.android.trisolarispms.ui.payu
|
||||||
|
|
||||||
|
data class PayuSettingsState(
|
||||||
|
val merchantKey: String = "",
|
||||||
|
val salt32: String = "",
|
||||||
|
val salt256: String = "",
|
||||||
|
val isTest: Boolean = false,
|
||||||
|
val useSalt256: Boolean = false,
|
||||||
|
val hasSalt32: Boolean = false,
|
||||||
|
val hasSalt256: Boolean = false,
|
||||||
|
val configured: Boolean = false,
|
||||||
|
val isLoading: Boolean = false,
|
||||||
|
val isSaving: Boolean = false,
|
||||||
|
val error: String? = null,
|
||||||
|
val message: String? = null
|
||||||
|
)
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
package com.android.trisolarispms.ui.payu
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.android.trisolarispms.data.api.ApiClient
|
||||||
|
import com.android.trisolarispms.data.api.model.PayuSettingsRequest
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class PayuSettingsViewModel : ViewModel() {
|
||||||
|
private val _state = MutableStateFlow(PayuSettingsState())
|
||||||
|
val state: StateFlow<PayuSettingsState> = _state
|
||||||
|
|
||||||
|
fun load(propertyId: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
loadInternal(propertyId = propertyId, showLoading = true, message = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onMerchantKeyChange(value: String) {
|
||||||
|
_state.update { it.copy(merchantKey = value, error = null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onSalt32Change(value: String) {
|
||||||
|
_state.update { it.copy(salt32 = value, error = null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onSalt256Change(value: String) {
|
||||||
|
_state.update { it.copy(salt256 = value, error = null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onIsTestChange(value: Boolean) {
|
||||||
|
_state.update { it.copy(isTest = value, error = null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onUseSalt256Change(value: Boolean) {
|
||||||
|
_state.update { it.copy(useSalt256 = value, error = null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun save(propertyId: String) {
|
||||||
|
val current = state.value
|
||||||
|
val merchantKey = current.merchantKey.trim()
|
||||||
|
val salt32 = current.salt32.trim()
|
||||||
|
val salt256 = current.salt256.trim()
|
||||||
|
|
||||||
|
if (merchantKey.isBlank()) {
|
||||||
|
_state.update { it.copy(error = "Merchant key is required") }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (current.useSalt256 && salt256.isBlank() && !current.hasSalt256) {
|
||||||
|
_state.update { it.copy(error = "Salt256 is required") }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!current.useSalt256 && salt32.isBlank() && !current.hasSalt32) {
|
||||||
|
_state.update { it.copy(error = "Salt32 is required") }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
_state.update { it.copy(isSaving = true, error = null, message = null) }
|
||||||
|
try {
|
||||||
|
val api = ApiClient.create()
|
||||||
|
val response = api.updatePayuSettings(
|
||||||
|
propertyId = propertyId,
|
||||||
|
body = PayuSettingsRequest(
|
||||||
|
merchantKey = merchantKey,
|
||||||
|
salt32 = salt32.ifBlank { null },
|
||||||
|
salt256 = salt256.ifBlank { null },
|
||||||
|
isTest = current.isTest,
|
||||||
|
useSalt256 = current.useSalt256
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
loadInternal(propertyId = propertyId, showLoading = false, message = "Saved")
|
||||||
|
} else {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isSaving = false,
|
||||||
|
error = "Save failed: ${response.code()}",
|
||||||
|
message = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isSaving = false,
|
||||||
|
error = e.localizedMessage ?: "Save failed",
|
||||||
|
message = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun loadInternal(propertyId: String, showLoading: Boolean, message: String?) {
|
||||||
|
if (showLoading) {
|
||||||
|
_state.update { it.copy(isLoading = true, error = null, message = message) }
|
||||||
|
} else {
|
||||||
|
_state.update { it.copy(error = null, message = message) }
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
val api = ApiClient.create()
|
||||||
|
val response = api.getPayuSettings(propertyId)
|
||||||
|
val body = response.body()
|
||||||
|
if (response.isSuccessful && body != null) {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
merchantKey = body.merchantKey.orEmpty(),
|
||||||
|
isTest = body.isTest == true,
|
||||||
|
useSalt256 = body.useSalt256 == true,
|
||||||
|
hasSalt32 = body.hasSalt32 == true,
|
||||||
|
hasSalt256 = body.hasSalt256 == true,
|
||||||
|
configured = body.configured == true,
|
||||||
|
salt32 = "",
|
||||||
|
salt256 = "",
|
||||||
|
isLoading = false,
|
||||||
|
isSaving = false,
|
||||||
|
error = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
isSaving = false,
|
||||||
|
error = "Load failed: ${response.code()}",
|
||||||
|
message = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
isSaving = false,
|
||||||
|
error = e.localizedMessage ?: "Load failed",
|
||||||
|
message = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.filled.ArrowBack
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.MeetingRoom
|
import androidx.compose.material.icons.filled.MeetingRoom
|
||||||
|
import androidx.compose.material.icons.filled.Payment
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
@@ -51,6 +52,8 @@ fun ActiveRoomStaysScreen(
|
|||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onViewRooms: () -> Unit,
|
onViewRooms: () -> Unit,
|
||||||
onCreateBooking: () -> Unit,
|
onCreateBooking: () -> Unit,
|
||||||
|
showPayuSettings: Boolean,
|
||||||
|
onPayuSettings: () -> Unit,
|
||||||
onManageRoomStay: (BookingListItem) -> Unit,
|
onManageRoomStay: (BookingListItem) -> Unit,
|
||||||
onViewBookingStays: (BookingListItem) -> Unit,
|
onViewBookingStays: (BookingListItem) -> Unit,
|
||||||
onOpenBookingDetails: (BookingListItem) -> Unit,
|
onOpenBookingDetails: (BookingListItem) -> Unit,
|
||||||
@@ -76,6 +79,11 @@ fun ActiveRoomStaysScreen(
|
|||||||
IconButton(onClick = onViewRooms) {
|
IconButton(onClick = onViewRooms) {
|
||||||
Icon(Icons.Default.MeetingRoom, contentDescription = "Available Rooms")
|
Icon(Icons.Default.MeetingRoom, contentDescription = "Available Rooms")
|
||||||
}
|
}
|
||||||
|
if (showPayuSettings) {
|
||||||
|
IconButton(onClick = onPayuSettings) {
|
||||||
|
Icon(Icons.Default.Payment, contentDescription = "Payu Settings")
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
colors = TopAppBarDefaults.topAppBarColors()
|
colors = TopAppBarDefaults.topAppBarColors()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,11 +5,14 @@ import androidx.compose.foundation.layout.Box
|
|||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.pager.HorizontalPager
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
@@ -17,6 +20,8 @@ import androidx.compose.foundation.verticalScroll
|
|||||||
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.Edit
|
import androidx.compose.material.icons.filled.Edit
|
||||||
|
import androidx.compose.material.icons.filled.QrCode
|
||||||
|
import androidx.compose.material.icons.filled.ReceiptLong
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
@@ -67,6 +72,8 @@ fun BookingDetailsTabsScreen(
|
|||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onEditCheckout: (String?, String?) -> Unit,
|
onEditCheckout: (String?, String?) -> Unit,
|
||||||
onEditSignature: (String) -> Unit,
|
onEditSignature: (String) -> Unit,
|
||||||
|
onOpenPayuQr: (Long?, String?) -> Unit,
|
||||||
|
onOpenPayments: () -> Unit,
|
||||||
staysViewModel: BookingRoomStaysViewModel = viewModel(),
|
staysViewModel: BookingRoomStaysViewModel = viewModel(),
|
||||||
detailsViewModel: BookingDetailsViewModel = viewModel()
|
detailsViewModel: BookingDetailsViewModel = viewModel()
|
||||||
) {
|
) {
|
||||||
@@ -126,7 +133,9 @@ fun BookingDetailsTabsScreen(
|
|||||||
isLoading = detailsState.isLoading,
|
isLoading = detailsState.isLoading,
|
||||||
error = detailsState.error,
|
error = detailsState.error,
|
||||||
onEditCheckout = onEditCheckout,
|
onEditCheckout = onEditCheckout,
|
||||||
onEditSignature = onEditSignature
|
onEditSignature = onEditSignature,
|
||||||
|
onOpenPayuQr = onOpenPayuQr,
|
||||||
|
onOpenPayments = onOpenPayments
|
||||||
)
|
)
|
||||||
1 -> BookingRoomStaysTabContent(staysState, staysViewModel)
|
1 -> BookingRoomStaysTabContent(staysState, staysViewModel)
|
||||||
}
|
}
|
||||||
@@ -143,7 +152,9 @@ private fun GuestInfoTabContent(
|
|||||||
isLoading: Boolean,
|
isLoading: Boolean,
|
||||||
error: String?,
|
error: String?,
|
||||||
onEditCheckout: (String?, String?) -> Unit,
|
onEditCheckout: (String?, String?) -> Unit,
|
||||||
onEditSignature: (String) -> Unit
|
onEditSignature: (String) -> Unit,
|
||||||
|
onOpenPayuQr: (Long?, String?) -> Unit,
|
||||||
|
onOpenPayments: () -> Unit
|
||||||
) {
|
) {
|
||||||
val displayZone = remember { ZoneId.of("Asia/Kolkata") }
|
val displayZone = remember { ZoneId.of("Asia/Kolkata") }
|
||||||
val dateFormatter = remember { DateTimeFormatter.ofPattern("dd/MM/yyyy") }
|
val dateFormatter = remember { DateTimeFormatter.ofPattern("dd/MM/yyyy") }
|
||||||
@@ -219,7 +230,33 @@ private fun GuestInfoTabContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SectionCard(title = "Calculations") {
|
val hasGuestName = details?.guestName?.isNotBlank() == true
|
||||||
|
val hasGuestPhone = details?.guestPhone?.isNotBlank() == true
|
||||||
|
val hasPending = (details?.pending ?: 0L) > 1L
|
||||||
|
SectionCard(
|
||||||
|
title = "Calculations",
|
||||||
|
headerContent = {
|
||||||
|
if (hasGuestName && hasGuestPhone && hasPending) {
|
||||||
|
IconButton(onClick = { onOpenPayuQr(details.pending, details.guestPhone) }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.QrCode,
|
||||||
|
contentDescription = "PayU QR",
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
IconButton(
|
||||||
|
onClick = onOpenPayments,
|
||||||
|
modifier = Modifier.size(36.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.ReceiptLong,
|
||||||
|
contentDescription = "Payments",
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
GuestDetailRow(label = "Amount Collected", value = details?.amountCollected?.toString())
|
GuestDetailRow(label = "Amount Collected", value = details?.amountCollected?.toString())
|
||||||
GuestDetailRow(label = "Rent per day", value = details?.totalNightlyRate?.toString())
|
GuestDetailRow(label = "Rent per day", value = details?.totalNightlyRate?.toString())
|
||||||
GuestDetailRow(label = "Expected pay", value = details?.expectedPay?.toString())
|
GuestDetailRow(label = "Expected pay", value = details?.expectedPay?.toString())
|
||||||
@@ -263,13 +300,24 @@ private fun GuestDetailRow(label: String, value: String?) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SectionCard(title: String, content: @Composable ColumnScope.() -> Unit) {
|
private fun SectionCard(
|
||||||
|
title: String,
|
||||||
|
headerContent: (@Composable RowScope.() -> Unit)? = null,
|
||||||
|
content: @Composable ColumnScope.() -> Unit
|
||||||
|
) {
|
||||||
Card(
|
Card(
|
||||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(16.dp)) {
|
Column(modifier = Modifier.padding(16.dp)) {
|
||||||
Text(text = title, style = MaterialTheme.typography.titleMedium)
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(text = title, style = MaterialTheme.typography.titleMedium)
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
headerContent?.invoke(this)
|
||||||
|
}
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ coilSvg = "2.7.0"
|
|||||||
lottieCompose = "6.7.1"
|
lottieCompose = "6.7.1"
|
||||||
calendarCompose = "2.6.0"
|
calendarCompose = "2.6.0"
|
||||||
libphonenumber = "8.13.34"
|
libphonenumber = "8.13.34"
|
||||||
|
zxingCore = "3.5.3"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
@@ -54,6 +55,7 @@ coil-svg = { group = "io.coil-kt", name = "coil-svg", version.ref = "coilSvg" }
|
|||||||
lottie-compose = { group = "com.airbnb.android", name = "lottie-compose", version.ref = "lottieCompose" }
|
lottie-compose = { group = "com.airbnb.android", name = "lottie-compose", version.ref = "lottieCompose" }
|
||||||
calendar-compose = { group = "com.kizitonwose.calendar", name = "compose", version.ref = "calendarCompose" }
|
calendar-compose = { group = "com.kizitonwose.calendar", name = "compose", version.ref = "calendarCompose" }
|
||||||
libphonenumber = { group = "com.googlecode.libphonenumber", name = "libphonenumber", version.ref = "libphonenumber" }
|
libphonenumber = { group = "com.googlecode.libphonenumber", name = "libphonenumber", version.ref = "libphonenumber" }
|
||||||
|
zxing-core = { group = "com.google.zxing", name = "core", version.ref = "zxingCore" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
|
|||||||
Reference in New Issue
Block a user