policy:time related
This commit is contained in:
35
.kotlin/errors/errors-1770004718940.log
Normal file
35
.kotlin/errors/errors-1770004718940.log
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
kotlin version: 2.3.0
|
||||||
|
error message: Incremental compilation failed: /home/androidlover5842/AndroidStudioProjects/TrisolarisPMS/app/build/kotlin/compileDebugKotlin/classpath-snapshot/shrunk-classpath-snapshot.bin (No such file or directory)
|
||||||
|
java.io.FileNotFoundException: /home/androidlover5842/AndroidStudioProjects/TrisolarisPMS/app/build/kotlin/compileDebugKotlin/classpath-snapshot/shrunk-classpath-snapshot.bin (No such file or directory)
|
||||||
|
at java.base/java.io.FileInputStream.open0(Native Method)
|
||||||
|
at java.base/java.io.FileInputStream.open(Unknown Source)
|
||||||
|
at java.base/java.io.FileInputStream.<init>(Unknown Source)
|
||||||
|
at org.jetbrains.kotlin.incremental.storage.ExternalizersKt.loadFromFile(externalizers.kt:184)
|
||||||
|
at org.jetbrains.kotlin.incremental.snapshots.LazyClasspathSnapshot.getSavedShrunkClasspathAgainstPreviousLookups(LazyClasspathSnapshot.kt:86)
|
||||||
|
at org.jetbrains.kotlin.incremental.classpathDiff.ClasspathSnapshotShrinkerKt.shrinkAndSaveClasspathSnapshot(ClasspathSnapshotShrinker.kt:267)
|
||||||
|
at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.performWorkAfterCompilation(IncrementalJvmCompilerRunner.kt:76)
|
||||||
|
at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.performWorkAfterCompilation(IncrementalJvmCompilerRunner.kt:23)
|
||||||
|
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileImpl(IncrementalCompilerRunner.kt:420)
|
||||||
|
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.tryCompileIncrementally$lambda$0$compile(IncrementalCompilerRunner.kt:249)
|
||||||
|
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.tryCompileIncrementally(IncrementalCompilerRunner.kt:267)
|
||||||
|
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:119)
|
||||||
|
at org.jetbrains.kotlin.daemon.CompileServiceImplBase.execIncrementalCompiler(CompileServiceImpl.kt:684)
|
||||||
|
at org.jetbrains.kotlin.daemon.CompileServiceImplBase.access$execIncrementalCompiler(CompileServiceImpl.kt:94)
|
||||||
|
at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1810)
|
||||||
|
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)
|
||||||
|
at java.base/java.lang.reflect.Method.invoke(Unknown Source)
|
||||||
|
at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(Unknown Source)
|
||||||
|
at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source)
|
||||||
|
at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source)
|
||||||
|
at java.base/java.security.AccessController.doPrivileged(Unknown Source)
|
||||||
|
at java.rmi/sun.rmi.transport.Transport.serviceCall(Unknown Source)
|
||||||
|
at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(Unknown Source)
|
||||||
|
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(Unknown Source)
|
||||||
|
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(Unknown Source)
|
||||||
|
at java.base/java.security.AccessController.doPrivileged(Unknown Source)
|
||||||
|
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(Unknown Source)
|
||||||
|
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
|
||||||
|
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
|
||||||
|
at java.base/java.lang.Thread.run(Unknown Source)
|
||||||
|
|
||||||
|
|
||||||
@@ -543,6 +543,7 @@ GET /properties/{propertyId}/bookings/{bookingId}/balance
|
|||||||
### Required project structure (current baseline)
|
### Required project structure (current baseline)
|
||||||
|
|
||||||
- `core/` -> cross-cutting business primitives/policies (e.g., auth policy, role enum).
|
- `core/` -> cross-cutting business primitives/policies (e.g., auth policy, role enum).
|
||||||
|
- `core/viewmodel/` -> shared ViewModel execution helpers (loading/error wrappers, common request runners).
|
||||||
- `data/api/core/` -> API client, constants, token providers, aggregated API service.
|
- `data/api/core/` -> API client, constants, token providers, aggregated API service.
|
||||||
- `data/api/service/` -> Retrofit endpoint interfaces only.
|
- `data/api/service/` -> Retrofit endpoint interfaces only.
|
||||||
- `data/api/model/` -> DTO/request/response models.
|
- `data/api/model/` -> DTO/request/response models.
|
||||||
@@ -558,6 +559,9 @@ GET /properties/{propertyId}/bookings/{bookingId}/balance
|
|||||||
5. Keep UI dumb: consume state and callbacks; avoid business rules in composables.
|
5. Keep UI dumb: consume state and callbacks; avoid business rules in composables.
|
||||||
6. If navigation changes, update `ui/navigation` only (single source of truth).
|
6. If navigation changes, update `ui/navigation` only (single source of truth).
|
||||||
7. Before finishing, remove any newly introduced duplication and compile-check.
|
7. Before finishing, remove any newly introduced duplication and compile-check.
|
||||||
|
8. If 2+ ViewModels repeat loading/error coroutine flow, extract/use shared helper in `core/viewmodel`.
|
||||||
|
9. If Add/Edit screens differ only by initialization + submit callback, extract a feature-local shared form screen.
|
||||||
|
10. Prefer dedupe/organization improvements even if net LOC does not decrease, as long as behavior remains unchanged.
|
||||||
|
|
||||||
### PR/refactor acceptance checklist
|
### PR/refactor acceptance checklist
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ class AuthzPolicy(
|
|||||||
|
|
||||||
fun canManageRazorpaySettings(propertyId: String): Boolean = hasRole(propertyId, Role.ADMIN)
|
fun canManageRazorpaySettings(propertyId: String): Boolean = hasRole(propertyId, Role.ADMIN)
|
||||||
|
|
||||||
|
fun canManageCancellationPolicy(propertyId: String): Boolean = hasRole(propertyId, Role.ADMIN)
|
||||||
|
|
||||||
|
fun canManageBillingPolicy(propertyId: String): Boolean = hasRole(propertyId, Role.ADMIN)
|
||||||
|
|
||||||
fun canDeleteCashPayment(propertyId: String): Boolean = hasRole(propertyId, Role.ADMIN)
|
fun canDeleteCashPayment(propertyId: String): Boolean = hasRole(propertyId, Role.ADMIN)
|
||||||
|
|
||||||
fun canAddBookingPayment(propertyId: String): Boolean =
|
fun canAddBookingPayment(propertyId: String): Boolean =
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ package com.android.trisolarispms.data.api.core
|
|||||||
|
|
||||||
import com.android.trisolarispms.data.api.service.AmenityApi
|
import com.android.trisolarispms.data.api.service.AmenityApi
|
||||||
import com.android.trisolarispms.data.api.service.AuthApi
|
import com.android.trisolarispms.data.api.service.AuthApi
|
||||||
|
import com.android.trisolarispms.data.api.service.BillingPolicyApi
|
||||||
import com.android.trisolarispms.data.api.service.BookingApi
|
import com.android.trisolarispms.data.api.service.BookingApi
|
||||||
|
import com.android.trisolarispms.data.api.service.CancellationPolicyApi
|
||||||
import com.android.trisolarispms.data.api.service.CardApi
|
import com.android.trisolarispms.data.api.service.CardApi
|
||||||
import com.android.trisolarispms.data.api.service.GuestApi
|
import com.android.trisolarispms.data.api.service.GuestApi
|
||||||
import com.android.trisolarispms.data.api.service.GuestDocumentApi
|
import com.android.trisolarispms.data.api.service.GuestDocumentApi
|
||||||
@@ -20,6 +22,7 @@ import com.android.trisolarispms.data.api.service.UserAdminApi
|
|||||||
|
|
||||||
interface ApiService :
|
interface ApiService :
|
||||||
AuthApi,
|
AuthApi,
|
||||||
|
BillingPolicyApi,
|
||||||
PropertyApi,
|
PropertyApi,
|
||||||
RoomTypeApi,
|
RoomTypeApi,
|
||||||
RoomApi,
|
RoomApi,
|
||||||
@@ -35,4 +38,5 @@ interface ApiService :
|
|||||||
AmenityApi,
|
AmenityApi,
|
||||||
RatePlanApi,
|
RatePlanApi,
|
||||||
RazorpaySettingsApi,
|
RazorpaySettingsApi,
|
||||||
|
CancellationPolicyApi,
|
||||||
UserAdminApi
|
UserAdminApi
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.android.trisolarispms.data.api.model
|
||||||
|
|
||||||
|
data class BillingPolicyRequest(
|
||||||
|
val billingCheckinTime: String,
|
||||||
|
val billingCheckoutTime: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class BillingPolicyResponse(
|
||||||
|
val propertyId: String? = null,
|
||||||
|
val billingCheckinTime: String? = null,
|
||||||
|
val billingCheckoutTime: String? = null
|
||||||
|
)
|
||||||
@@ -1,5 +1,20 @@
|
|||||||
package com.android.trisolarispms.data.api.model
|
package com.android.trisolarispms.data.api.model
|
||||||
|
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
enum class BookingBillingMode {
|
||||||
|
PROPERTY_POLICY,
|
||||||
|
CUSTOM_WINDOW,
|
||||||
|
FULL_24H;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun from(value: String?): BookingBillingMode? {
|
||||||
|
val normalized = value?.trim()?.uppercase(Locale.US) ?: return null
|
||||||
|
return entries.firstOrNull { it.name == normalized }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data class BookingCheckInRequest(
|
data class BookingCheckInRequest(
|
||||||
val roomIds: List<String>,
|
val roomIds: List<String>,
|
||||||
val checkInAt: String? = null,
|
val checkInAt: String? = null,
|
||||||
@@ -11,6 +26,8 @@ data class BookingCheckInRequest(
|
|||||||
data class BookingCreateRequest(
|
data class BookingCreateRequest(
|
||||||
val expectedCheckInAt: String,
|
val expectedCheckInAt: String,
|
||||||
val expectedCheckOutAt: String,
|
val expectedCheckOutAt: String,
|
||||||
|
val billingMode: BookingBillingMode? = null,
|
||||||
|
val billingCheckoutTime: String? = null,
|
||||||
val source: String? = null,
|
val source: String? = null,
|
||||||
val guestPhoneE164: String? = null,
|
val guestPhoneE164: String? = null,
|
||||||
val fromCity: String? = null,
|
val fromCity: String? = null,
|
||||||
@@ -57,11 +74,16 @@ data class BookingListItem(
|
|||||||
val expectedGuestCount: Int? = null,
|
val expectedGuestCount: Int? = null,
|
||||||
val notes: String? = null
|
val notes: String? = null
|
||||||
,
|
,
|
||||||
val pending: Long? = null
|
val pending: Long? = null,
|
||||||
|
val billingMode: String? = null,
|
||||||
|
val billingCheckinTime: String? = null,
|
||||||
|
val billingCheckoutTime: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
data class BookingBulkCheckInRequest(
|
data class BookingBulkCheckInRequest(
|
||||||
val stays: List<BookingBulkCheckInStayRequest>
|
val stays: List<BookingBulkCheckInStayRequest>,
|
||||||
|
val transportMode: String? = null,
|
||||||
|
val notes: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
data class BookingBulkCheckInStayRequest(
|
data class BookingBulkCheckInStayRequest(
|
||||||
@@ -95,6 +117,7 @@ data class BookingDetailsResponse(
|
|||||||
val guestSignatureUrl: String? = null,
|
val guestSignatureUrl: String? = null,
|
||||||
val vehicleNumbers: List<String> = emptyList(),
|
val vehicleNumbers: List<String> = emptyList(),
|
||||||
val roomNumbers: List<Int> = emptyList(),
|
val roomNumbers: List<Int> = emptyList(),
|
||||||
|
val source: String? = null,
|
||||||
val fromCity: String? = null,
|
val fromCity: String? = null,
|
||||||
val toCity: String? = null,
|
val toCity: String? = null,
|
||||||
val memberRelation: String? = null,
|
val memberRelation: String? = null,
|
||||||
@@ -110,11 +133,20 @@ data class BookingDetailsResponse(
|
|||||||
val totalGuestCount: Int? = null,
|
val totalGuestCount: Int? = null,
|
||||||
val expectedGuestCount: Int? = null,
|
val expectedGuestCount: Int? = null,
|
||||||
val totalNightlyRate: Long? = null,
|
val totalNightlyRate: Long? = null,
|
||||||
|
val notes: String? = null,
|
||||||
val registeredByName: String? = null,
|
val registeredByName: String? = null,
|
||||||
val registeredByPhone: String? = null,
|
val registeredByPhone: String? = null,
|
||||||
val expectedPay: Long? = null,
|
val expectedPay: Long? = null,
|
||||||
val amountCollected: Long? = null,
|
val amountCollected: Long? = null,
|
||||||
val pending: Long? = null
|
val pending: Long? = null,
|
||||||
|
val billingMode: String? = null,
|
||||||
|
val billingCheckinTime: String? = null,
|
||||||
|
val billingCheckoutTime: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class BookingBillingPolicyUpdateRequest(
|
||||||
|
val billingMode: BookingBillingMode,
|
||||||
|
val billingCheckoutTime: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
data class BookingLinkGuestRequest(
|
data class BookingLinkGuestRequest(
|
||||||
@@ -126,6 +158,11 @@ data class BookingCheckOutRequest(
|
|||||||
val notes: String? = null
|
val notes: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class BookingRoomStayCheckOutRequest(
|
||||||
|
val checkOutAt: String? = null,
|
||||||
|
val notes: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
data class BookingCancelRequest(
|
data class BookingCancelRequest(
|
||||||
val cancelledAt: String? = null,
|
val cancelledAt: String? = null,
|
||||||
val reason: String? = null
|
val reason: String? = null
|
||||||
@@ -136,39 +173,12 @@ data class BookingNoShowRequest(
|
|||||||
val reason: String? = null
|
val reason: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
data class BookingRoomStayCreateRequest(
|
data class BookingBalanceResponse(
|
||||||
val roomId: String,
|
val expectedPay: Long? = null,
|
||||||
val fromAt: String,
|
val amountCollected: Long? = null,
|
||||||
val toAt: String,
|
val pending: Long? = null
|
||||||
val notes: String? = null
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Room Stays
|
data class RoomStayVoidRequest(
|
||||||
|
val reason: String
|
||||||
data class RoomStayCreateRequest(
|
|
||||||
val roomId: String,
|
|
||||||
val guestId: String? = null,
|
|
||||||
val checkIn: String? = null,
|
|
||||||
val checkOut: String? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
data class RoomStayDto(
|
|
||||||
val id: String? = null,
|
|
||||||
val bookingId: String? = null,
|
|
||||||
val roomId: String? = null,
|
|
||||||
val status: String? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
data class RoomChangeRequest(
|
|
||||||
val newRoomId: String,
|
|
||||||
val movedAt: String? = null,
|
|
||||||
val idempotencyKey: String
|
|
||||||
)
|
|
||||||
|
|
||||||
data class RoomChangeResponse(
|
|
||||||
val oldRoomStayId: String? = null,
|
|
||||||
val newRoomStayId: String? = null,
|
|
||||||
val oldRoomId: String? = null,
|
|
||||||
val newRoomId: String? = null,
|
|
||||||
val movedAt: String? = null
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.android.trisolarispms.data.api.model
|
||||||
|
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
enum class CancellationPenaltyMode {
|
||||||
|
NO_CHARGE,
|
||||||
|
ONE_NIGHT,
|
||||||
|
FULL_STAY;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun from(value: String?): CancellationPenaltyMode? {
|
||||||
|
val normalized = value?.trim()?.uppercase(Locale.US) ?: return null
|
||||||
|
return entries.firstOrNull { it.name == normalized }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class CancellationPolicyRequest(
|
||||||
|
val freeDaysBeforeCheckin: Int,
|
||||||
|
val penaltyMode: CancellationPenaltyMode
|
||||||
|
)
|
||||||
|
|
||||||
|
data class CancellationPolicyResponse(
|
||||||
|
val freeDaysBeforeCheckin: Int? = null,
|
||||||
|
val penaltyMode: String? = null
|
||||||
|
)
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.android.trisolarispms.data.api.service
|
||||||
|
|
||||||
|
import com.android.trisolarispms.data.api.model.BillingPolicyRequest
|
||||||
|
import com.android.trisolarispms.data.api.model.BillingPolicyResponse
|
||||||
|
import retrofit2.Response
|
||||||
|
import retrofit2.http.Body
|
||||||
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.PUT
|
||||||
|
import retrofit2.http.Path
|
||||||
|
|
||||||
|
interface BillingPolicyApi {
|
||||||
|
@GET("properties/{propertyId}/billing-policy")
|
||||||
|
suspend fun getBillingPolicy(
|
||||||
|
@Path("propertyId") propertyId: String
|
||||||
|
): Response<BillingPolicyResponse>
|
||||||
|
|
||||||
|
@PUT("properties/{propertyId}/billing-policy")
|
||||||
|
suspend fun updateBillingPolicy(
|
||||||
|
@Path("propertyId") propertyId: String,
|
||||||
|
@Body body: BillingPolicyRequest
|
||||||
|
): Response<BillingPolicyResponse>
|
||||||
|
}
|
||||||
@@ -4,16 +4,17 @@ import com.android.trisolarispms.data.api.model.ActionResponse
|
|||||||
import com.android.trisolarispms.data.api.model.BookingCancelRequest
|
import com.android.trisolarispms.data.api.model.BookingCancelRequest
|
||||||
import com.android.trisolarispms.data.api.model.BookingCheckInRequest
|
import com.android.trisolarispms.data.api.model.BookingCheckInRequest
|
||||||
import com.android.trisolarispms.data.api.model.BookingCheckOutRequest
|
import com.android.trisolarispms.data.api.model.BookingCheckOutRequest
|
||||||
|
import com.android.trisolarispms.data.api.model.BookingRoomStayCheckOutRequest
|
||||||
import com.android.trisolarispms.data.api.model.BookingCreateRequest
|
import com.android.trisolarispms.data.api.model.BookingCreateRequest
|
||||||
import com.android.trisolarispms.data.api.model.BookingCreateResponse
|
import com.android.trisolarispms.data.api.model.BookingCreateResponse
|
||||||
import com.android.trisolarispms.data.api.model.BookingLinkGuestRequest
|
import com.android.trisolarispms.data.api.model.BookingLinkGuestRequest
|
||||||
import com.android.trisolarispms.data.api.model.BookingNoShowRequest
|
import com.android.trisolarispms.data.api.model.BookingNoShowRequest
|
||||||
import com.android.trisolarispms.data.api.model.BookingRoomStayCreateRequest
|
|
||||||
import com.android.trisolarispms.data.api.model.BookingListItem
|
import com.android.trisolarispms.data.api.model.BookingListItem
|
||||||
import com.android.trisolarispms.data.api.model.BookingBulkCheckInRequest
|
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.BookingBalanceResponse
|
||||||
|
import com.android.trisolarispms.data.api.model.BookingBillingPolicyUpdateRequest
|
||||||
import com.android.trisolarispms.data.api.model.RazorpayQrEventDto
|
import com.android.trisolarispms.data.api.model.RazorpayQrEventDto
|
||||||
import com.android.trisolarispms.data.api.model.RazorpayRequestListItemDto
|
import com.android.trisolarispms.data.api.model.RazorpayRequestListItemDto
|
||||||
import com.android.trisolarispms.data.api.model.RazorpayQrRequest
|
import com.android.trisolarispms.data.api.model.RazorpayQrRequest
|
||||||
@@ -59,6 +60,13 @@ interface BookingApi {
|
|||||||
@Body body: BookingExpectedDatesRequest
|
@Body body: BookingExpectedDatesRequest
|
||||||
): Response<Unit>
|
): Response<Unit>
|
||||||
|
|
||||||
|
@POST("properties/{propertyId}/bookings/{bookingId}/billing-policy")
|
||||||
|
suspend fun updateBookingBillingPolicy(
|
||||||
|
@Path("propertyId") propertyId: String,
|
||||||
|
@Path("bookingId") bookingId: String,
|
||||||
|
@Body body: BookingBillingPolicyUpdateRequest
|
||||||
|
): Response<Unit>
|
||||||
|
|
||||||
@GET("properties/{propertyId}/bookings/{bookingId}")
|
@GET("properties/{propertyId}/bookings/{bookingId}")
|
||||||
suspend fun getBookingDetails(
|
suspend fun getBookingDetails(
|
||||||
@Path("propertyId") propertyId: String,
|
@Path("propertyId") propertyId: String,
|
||||||
@@ -70,7 +78,7 @@ interface BookingApi {
|
|||||||
@Path("propertyId") propertyId: String,
|
@Path("propertyId") propertyId: String,
|
||||||
@Path("bookingId") bookingId: String,
|
@Path("bookingId") bookingId: String,
|
||||||
@Body body: BookingLinkGuestRequest
|
@Body body: BookingLinkGuestRequest
|
||||||
): Response<BookingCreateResponse>
|
): Response<Unit>
|
||||||
|
|
||||||
@POST("properties/{propertyId}/bookings/{bookingId}/check-in")
|
@POST("properties/{propertyId}/bookings/{bookingId}/check-in")
|
||||||
suspend fun checkIn(
|
suspend fun checkIn(
|
||||||
@@ -84,28 +92,35 @@ interface BookingApi {
|
|||||||
@Path("propertyId") propertyId: String,
|
@Path("propertyId") propertyId: String,
|
||||||
@Path("bookingId") bookingId: String,
|
@Path("bookingId") bookingId: String,
|
||||||
@Body body: BookingCheckOutRequest
|
@Body body: BookingCheckOutRequest
|
||||||
): Response<ActionResponse>
|
): Response<Unit>
|
||||||
|
|
||||||
|
@POST("properties/{propertyId}/bookings/{bookingId}/room-stays/{roomStayId}/check-out")
|
||||||
|
suspend fun checkOutRoomStay(
|
||||||
|
@Path("propertyId") propertyId: String,
|
||||||
|
@Path("bookingId") bookingId: String,
|
||||||
|
@Path("roomStayId") roomStayId: String,
|
||||||
|
@Body body: BookingRoomStayCheckOutRequest
|
||||||
|
): Response<Unit>
|
||||||
|
|
||||||
@POST("properties/{propertyId}/bookings/{bookingId}/cancel")
|
@POST("properties/{propertyId}/bookings/{bookingId}/cancel")
|
||||||
suspend fun cancelBooking(
|
suspend fun cancelBooking(
|
||||||
@Path("propertyId") propertyId: String,
|
@Path("propertyId") propertyId: String,
|
||||||
@Path("bookingId") bookingId: String,
|
@Path("bookingId") bookingId: String,
|
||||||
@Body body: BookingCancelRequest
|
@Body body: BookingCancelRequest
|
||||||
): Response<ActionResponse>
|
): Response<Unit>
|
||||||
|
|
||||||
@POST("properties/{propertyId}/bookings/{bookingId}/no-show")
|
@POST("properties/{propertyId}/bookings/{bookingId}/no-show")
|
||||||
suspend fun noShow(
|
suspend fun noShow(
|
||||||
@Path("propertyId") propertyId: String,
|
@Path("propertyId") propertyId: String,
|
||||||
@Path("bookingId") bookingId: String,
|
@Path("bookingId") bookingId: String,
|
||||||
@Body body: BookingNoShowRequest
|
@Body body: BookingNoShowRequest
|
||||||
): Response<ActionResponse>
|
): Response<Unit>
|
||||||
|
|
||||||
@POST("properties/{propertyId}/bookings/{bookingId}/room-stays")
|
@GET("properties/{propertyId}/bookings/{bookingId}/balance")
|
||||||
suspend fun preAssignRoomStay(
|
suspend fun getBookingBalance(
|
||||||
@Path("propertyId") propertyId: String,
|
@Path("propertyId") propertyId: String,
|
||||||
@Path("bookingId") bookingId: String,
|
@Path("bookingId") bookingId: String
|
||||||
@Body body: BookingRoomStayCreateRequest
|
): Response<BookingBalanceResponse>
|
||||||
): Response<RoomStayDto>
|
|
||||||
|
|
||||||
@POST("properties/{propertyId}/bookings/{bookingId}/payments/razorpay/qr")
|
@POST("properties/{propertyId}/bookings/{bookingId}/payments/razorpay/qr")
|
||||||
suspend fun generateRazorpayQr(
|
suspend fun generateRazorpayQr(
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.android.trisolarispms.data.api.service
|
||||||
|
|
||||||
|
import com.android.trisolarispms.data.api.model.CancellationPolicyRequest
|
||||||
|
import com.android.trisolarispms.data.api.model.CancellationPolicyResponse
|
||||||
|
import retrofit2.Response
|
||||||
|
import retrofit2.http.Body
|
||||||
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.PUT
|
||||||
|
import retrofit2.http.Path
|
||||||
|
|
||||||
|
interface CancellationPolicyApi {
|
||||||
|
@GET("properties/{propertyId}/cancellation-policy")
|
||||||
|
suspend fun getCancellationPolicy(
|
||||||
|
@Path("propertyId") propertyId: String
|
||||||
|
): Response<CancellationPolicyResponse>
|
||||||
|
|
||||||
|
@PUT("properties/{propertyId}/cancellation-policy")
|
||||||
|
suspend fun updateCancellationPolicy(
|
||||||
|
@Path("propertyId") propertyId: String,
|
||||||
|
@Body body: CancellationPolicyRequest
|
||||||
|
): Response<CancellationPolicyResponse>
|
||||||
|
}
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
package com.android.trisolarispms.data.api.service
|
package com.android.trisolarispms.data.api.service
|
||||||
|
|
||||||
import com.android.trisolarispms.data.api.model.RoomChangeRequest
|
|
||||||
import com.android.trisolarispms.data.api.model.RoomChangeResponse
|
|
||||||
import com.android.trisolarispms.data.api.model.ActiveRoomStayDto
|
import com.android.trisolarispms.data.api.model.ActiveRoomStayDto
|
||||||
|
import com.android.trisolarispms.data.api.model.RoomStayVoidRequest
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
import retrofit2.http.Body
|
import retrofit2.http.Body
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
@@ -10,13 +9,13 @@ import retrofit2.http.POST
|
|||||||
import retrofit2.http.Path
|
import retrofit2.http.Path
|
||||||
|
|
||||||
interface RoomStayApi {
|
interface RoomStayApi {
|
||||||
@POST("properties/{propertyId}/room-stays/{roomStayId}/change-room")
|
|
||||||
suspend fun changeRoom(
|
|
||||||
@Path("propertyId") propertyId: String,
|
|
||||||
@Path("roomStayId") roomStayId: String,
|
|
||||||
@Body body: RoomChangeRequest
|
|
||||||
): Response<RoomChangeResponse>
|
|
||||||
|
|
||||||
@GET("properties/{propertyId}/room-stays/active")
|
@GET("properties/{propertyId}/room-stays/active")
|
||||||
suspend fun listActiveRoomStays(@Path("propertyId") propertyId: String): Response<List<ActiveRoomStayDto>>
|
suspend fun listActiveRoomStays(@Path("propertyId") propertyId: String): Response<List<ActiveRoomStayDto>>
|
||||||
|
|
||||||
|
@POST("properties/{propertyId}/room-stays/{roomStayId}/void")
|
||||||
|
suspend fun voidRoomStay(
|
||||||
|
@Path("propertyId") propertyId: String,
|
||||||
|
@Path("roomStayId") roomStayId: String,
|
||||||
|
@Body body: RoomStayVoidRequest
|
||||||
|
): Response<Unit>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.android.trisolarispms.ui.booking
|
package com.android.trisolarispms.ui.booking
|
||||||
|
|
||||||
|
import android.app.TimePickerDialog
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -17,6 +18,7 @@ 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.CalendarMonth
|
import androidx.compose.material.icons.filled.CalendarMonth
|
||||||
|
import androidx.compose.material.icons.filled.Schedule
|
||||||
import androidx.compose.material.icons.filled.Done
|
import androidx.compose.material.icons.filled.Done
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
@@ -43,9 +45,11 @@ import androidx.compose.runtime.remember
|
|||||||
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.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
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
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.android.trisolarispms.data.api.model.BookingBillingMode
|
||||||
import com.kizitonwose.calendar.compose.HorizontalCalendar
|
import com.kizitonwose.calendar.compose.HorizontalCalendar
|
||||||
import com.kizitonwose.calendar.compose.rememberCalendarState
|
import com.kizitonwose.calendar.compose.rememberCalendarState
|
||||||
import com.kizitonwose.calendar.core.CalendarDay
|
import com.kizitonwose.calendar.core.CalendarDay
|
||||||
@@ -81,6 +85,7 @@ fun BookingCreateScreen(
|
|||||||
val relationOptions = listOf("FRIENDS", "FAMILY", "GROUP", "ALONE")
|
val relationOptions = listOf("FRIENDS", "FAMILY", "GROUP", "ALONE")
|
||||||
val transportMenuExpanded = remember { mutableStateOf(false) }
|
val transportMenuExpanded = remember { mutableStateOf(false) }
|
||||||
val transportOptions = listOf("CAR", "BIKE", "TRAIN", "PLANE", "BUS", "FOOT", "CYCLE", "OTHER")
|
val transportOptions = listOf("CAR", "BIKE", "TRAIN", "PLANE", "BUS", "FOOT", "CYCLE", "OTHER")
|
||||||
|
val billingModeMenuExpanded = remember { mutableStateOf(false) }
|
||||||
val displayFormatter = remember { DateTimeFormatter.ofPattern("dd-MM-yy HH:mm") }
|
val displayFormatter = remember { DateTimeFormatter.ofPattern("dd-MM-yy HH:mm") }
|
||||||
val phoneCountryMenuExpanded = remember { mutableStateOf(false) }
|
val phoneCountryMenuExpanded = remember { mutableStateOf(false) }
|
||||||
val phoneCountries = remember { phoneCountryOptions() }
|
val phoneCountries = remember { phoneCountryOptions() }
|
||||||
@@ -88,10 +93,13 @@ fun BookingCreateScreen(
|
|||||||
|
|
||||||
LaunchedEffect(propertyId) {
|
LaunchedEffect(propertyId) {
|
||||||
viewModel.reset()
|
viewModel.reset()
|
||||||
|
viewModel.loadBillingPolicy(propertyId)
|
||||||
val now = OffsetDateTime.now()
|
val now = OffsetDateTime.now()
|
||||||
checkInDate.value = now.toLocalDate()
|
checkInDate.value = now.toLocalDate()
|
||||||
checkInTime.value = now.format(DateTimeFormatter.ofPattern("HH:mm"))
|
checkInTime.value = now.format(DateTimeFormatter.ofPattern("HH:mm"))
|
||||||
viewModel.onExpectedCheckInAtChange(now.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME))
|
val nowIso = now.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)
|
||||||
|
viewModel.onExpectedCheckInAtChange(nowIso)
|
||||||
|
viewModel.autoSetBillingFromCheckIn(nowIso)
|
||||||
checkInNow.value = true
|
checkInNow.value = true
|
||||||
val defaultCheckoutDate = now.toLocalDate().plusDays(1)
|
val defaultCheckoutDate = now.toLocalDate().plusDays(1)
|
||||||
checkOutDate.value = defaultCheckoutDate
|
checkOutDate.value = defaultCheckoutDate
|
||||||
@@ -139,7 +147,9 @@ fun BookingCreateScreen(
|
|||||||
val now = OffsetDateTime.now()
|
val now = OffsetDateTime.now()
|
||||||
checkInDate.value = now.toLocalDate()
|
checkInDate.value = now.toLocalDate()
|
||||||
checkInTime.value = now.format(DateTimeFormatter.ofPattern("HH:mm"))
|
checkInTime.value = now.format(DateTimeFormatter.ofPattern("HH:mm"))
|
||||||
viewModel.onExpectedCheckInAtChange(now.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME))
|
val nowIso = now.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)
|
||||||
|
viewModel.onExpectedCheckInAtChange(nowIso)
|
||||||
|
viewModel.autoSetBillingFromCheckIn(nowIso)
|
||||||
} else {
|
} else {
|
||||||
viewModel.onExpectedCheckInAtChange("")
|
viewModel.onExpectedCheckInAtChange("")
|
||||||
}
|
}
|
||||||
@@ -179,6 +189,47 @@ fun BookingCreateScreen(
|
|||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
ExposedDropdownMenuBox(
|
||||||
|
expanded = billingModeMenuExpanded.value,
|
||||||
|
onExpandedChange = { billingModeMenuExpanded.value = !billingModeMenuExpanded.value }
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state.billingMode.name,
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true,
|
||||||
|
label = { Text("Billing Mode") },
|
||||||
|
trailingIcon = {
|
||||||
|
ExposedDropdownMenuDefaults.TrailingIcon(expanded = billingModeMenuExpanded.value)
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.menuAnchor()
|
||||||
|
)
|
||||||
|
ExposedDropdownMenu(
|
||||||
|
expanded = billingModeMenuExpanded.value,
|
||||||
|
onDismissRequest = { billingModeMenuExpanded.value = false }
|
||||||
|
) {
|
||||||
|
BookingBillingMode.entries.forEach { mode ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(mode.name) },
|
||||||
|
onClick = {
|
||||||
|
billingModeMenuExpanded.value = false
|
||||||
|
viewModel.onBillingModeChange(mode)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (state.billingMode == BookingBillingMode.CUSTOM_WINDOW) {
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
TimePickerTextField(
|
||||||
|
value = state.billingCheckoutTime,
|
||||||
|
label = { Text("Billing check-out (HH:mm)") },
|
||||||
|
onTimeSelected = viewModel::onBillingCheckoutTimeChange,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
val selectedCountry = findPhoneCountryOption(state.phoneCountryCode)
|
val selectedCountry = findPhoneCountryOption(state.phoneCountryCode)
|
||||||
val phoneDigitsLength = state.phoneNationalNumber.length
|
val phoneDigitsLength = state.phoneNationalNumber.length
|
||||||
val phoneIsComplete = phoneDigitsLength == selectedCountry.maxLength
|
val phoneIsComplete = phoneDigitsLength == selectedCountry.maxLength
|
||||||
@@ -491,6 +542,58 @@ fun BookingCreateScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TimePickerTextField(
|
||||||
|
value: String,
|
||||||
|
label: @Composable () -> Unit,
|
||||||
|
onTimeSelected: (String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val parsed = runCatching {
|
||||||
|
val parts = value.split(":")
|
||||||
|
val hour = parts.getOrNull(0)?.toIntOrNull() ?: 12
|
||||||
|
val minute = parts.getOrNull(1)?.toIntOrNull() ?: 0
|
||||||
|
(hour.coerceIn(0, 23)) to (minute.coerceIn(0, 59))
|
||||||
|
}.getOrDefault(12 to 0)
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = value,
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true,
|
||||||
|
label = label,
|
||||||
|
trailingIcon = {
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
TimePickerDialog(
|
||||||
|
context,
|
||||||
|
{ _, hourOfDay, minute ->
|
||||||
|
onTimeSelected("%02d:%02d".format(hourOfDay, minute))
|
||||||
|
},
|
||||||
|
parsed.first,
|
||||||
|
parsed.second,
|
||||||
|
true
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.Schedule, contentDescription = "Pick time")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = modifier
|
||||||
|
.clickable {
|
||||||
|
TimePickerDialog(
|
||||||
|
context,
|
||||||
|
{ _, hourOfDay, minute ->
|
||||||
|
onTimeSelected("%02d:%02d".format(hourOfDay, minute))
|
||||||
|
},
|
||||||
|
parsed.first,
|
||||||
|
parsed.second,
|
||||||
|
true
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun DateTimePickerDialog(
|
private fun DateTimePickerDialog(
|
||||||
title: String,
|
title: String,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.android.trisolarispms.ui.booking
|
package com.android.trisolarispms.ui.booking
|
||||||
|
|
||||||
|
import com.android.trisolarispms.data.api.model.BookingBillingMode
|
||||||
|
|
||||||
data class BookingCreateState(
|
data class BookingCreateState(
|
||||||
val phoneCountryCode: String = "IN",
|
val phoneCountryCode: String = "IN",
|
||||||
val phoneNationalNumber: String = "",
|
val phoneNationalNumber: String = "",
|
||||||
@@ -8,6 +10,8 @@ data class BookingCreateState(
|
|||||||
val phoneVisitCountPhone: String? = null,
|
val phoneVisitCountPhone: String? = null,
|
||||||
val expectedCheckInAt: String = "",
|
val expectedCheckInAt: String = "",
|
||||||
val expectedCheckOutAt: String = "",
|
val expectedCheckOutAt: String = "",
|
||||||
|
val billingMode: BookingBillingMode = BookingBillingMode.PROPERTY_POLICY,
|
||||||
|
val billingCheckoutTime: String = "",
|
||||||
val source: String = "WALKIN",
|
val source: String = "WALKIN",
|
||||||
val fromCity: String = "",
|
val fromCity: String = "",
|
||||||
val toCity: String = "",
|
val toCity: String = "",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.android.trisolarispms.ui.booking
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.android.trisolarispms.data.api.core.ApiClient
|
import com.android.trisolarispms.data.api.core.ApiClient
|
||||||
|
import com.android.trisolarispms.data.api.model.BookingBillingMode
|
||||||
import com.android.trisolarispms.data.api.model.BookingCreateRequest
|
import com.android.trisolarispms.data.api.model.BookingCreateRequest
|
||||||
import com.android.trisolarispms.data.api.model.BookingCreateResponse
|
import com.android.trisolarispms.data.api.model.BookingCreateResponse
|
||||||
import com.android.trisolarispms.data.api.model.GuestDto
|
import com.android.trisolarispms.data.api.model.GuestDto
|
||||||
@@ -10,6 +11,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
class BookingCreateViewModel : ViewModel() {
|
class BookingCreateViewModel : ViewModel() {
|
||||||
private val _state = MutableStateFlow(BookingCreateState())
|
private val _state = MutableStateFlow(BookingCreateState())
|
||||||
@@ -27,6 +29,53 @@ class BookingCreateViewModel : ViewModel() {
|
|||||||
_state.update { it.copy(expectedCheckOutAt = value, error = null) }
|
_state.update { it.copy(expectedCheckOutAt = value, error = null) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun autoSetBillingFromCheckIn(checkInAtIso: String) {
|
||||||
|
val checkIn = runCatching { OffsetDateTime.parse(checkInAtIso) }.getOrNull() ?: return
|
||||||
|
val hour = checkIn.hour
|
||||||
|
if (hour in 0..4) {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
billingMode = BookingBillingMode.CUSTOM_WINDOW,
|
||||||
|
billingCheckoutTime = "17:00",
|
||||||
|
error = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (hour in 5..11) {
|
||||||
|
_state.update { it.copy(billingMode = BookingBillingMode.FULL_24H, error = null) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadBillingPolicy(propertyId: String) {
|
||||||
|
if (propertyId.isBlank()) return
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
val api = ApiClient.create()
|
||||||
|
val response = api.getBillingPolicy(propertyId)
|
||||||
|
val body = response.body()
|
||||||
|
if (response.isSuccessful && body != null) {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
billingCheckoutTime = body.billingCheckoutTime.orEmpty(),
|
||||||
|
error = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
// Keep defaults; billing policy can still be set manually.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onBillingModeChange(value: BookingBillingMode) {
|
||||||
|
_state.update { it.copy(billingMode = value, error = null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onBillingCheckoutTimeChange(value: String) {
|
||||||
|
_state.update { it.copy(billingCheckoutTime = value, error = null) }
|
||||||
|
}
|
||||||
|
|
||||||
fun onPhoneCountryChange(value: String) {
|
fun onPhoneCountryChange(value: String) {
|
||||||
val option = findPhoneCountryOption(value)
|
val option = findPhoneCountryOption(value)
|
||||||
_state.update { current ->
|
_state.update { current ->
|
||||||
@@ -132,6 +181,14 @@ class BookingCreateViewModel : ViewModel() {
|
|||||||
_state.update { it.copy(error = "Check-in and check-out are required") }
|
_state.update { it.copy(error = "Check-in and check-out are required") }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
val hhmmRegex = Regex("^([01]\\d|2[0-3]):[0-5]\\d$")
|
||||||
|
val billingCheckout = current.billingCheckoutTime.trim()
|
||||||
|
if (current.billingMode == BookingBillingMode.CUSTOM_WINDOW) {
|
||||||
|
if (!hhmmRegex.matches(billingCheckout)) {
|
||||||
|
_state.update { it.copy(error = "Billing checkout time must be HH:mm") }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
val childCount = current.childCount.toIntOrNull()
|
val childCount = current.childCount.toIntOrNull()
|
||||||
val maleCount = current.maleCount.toIntOrNull()
|
val maleCount = current.maleCount.toIntOrNull()
|
||||||
val femaleCount = current.femaleCount.toIntOrNull()
|
val femaleCount = current.femaleCount.toIntOrNull()
|
||||||
@@ -152,6 +209,12 @@ class BookingCreateViewModel : ViewModel() {
|
|||||||
body = BookingCreateRequest(
|
body = BookingCreateRequest(
|
||||||
expectedCheckInAt = checkIn,
|
expectedCheckInAt = checkIn,
|
||||||
expectedCheckOutAt = checkOut,
|
expectedCheckOutAt = checkOut,
|
||||||
|
billingMode = current.billingMode,
|
||||||
|
billingCheckoutTime = when (current.billingMode) {
|
||||||
|
BookingBillingMode.CUSTOM_WINDOW -> billingCheckout
|
||||||
|
BookingBillingMode.PROPERTY_POLICY -> null
|
||||||
|
BookingBillingMode.FULL_24H -> null
|
||||||
|
},
|
||||||
source = current.source.trim().ifBlank { null },
|
source = current.source.trim().ifBlank { null },
|
||||||
guestPhoneE164 = phone,
|
guestPhoneE164 = phone,
|
||||||
fromCity = current.fromCity.trim().ifBlank { null },
|
fromCity = current.fromCity.trim().ifBlank { null },
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ sealed interface AppRoute {
|
|||||||
val ratePlanCode: String
|
val ratePlanCode: String
|
||||||
) : AppRoute
|
) : AppRoute
|
||||||
data class RazorpaySettings(val propertyId: String) : AppRoute
|
data class RazorpaySettings(val propertyId: String) : AppRoute
|
||||||
|
data class PropertySettings(val propertyId: String) : AppRoute
|
||||||
data class RazorpayQr(
|
data class RazorpayQr(
|
||||||
val propertyId: String,
|
val propertyId: String,
|
||||||
val bookingId: String,
|
val bookingId: String,
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ internal fun handleBackNavigation(
|
|||||||
currentRoute.roomTypeId
|
currentRoute.roomTypeId
|
||||||
)
|
)
|
||||||
is AppRoute.RazorpaySettings -> refs.openActiveRoomStays(currentRoute.propertyId)
|
is AppRoute.RazorpaySettings -> refs.openActiveRoomStays(currentRoute.propertyId)
|
||||||
|
is AppRoute.PropertySettings -> refs.openActiveRoomStays(currentRoute.propertyId)
|
||||||
is AppRoute.RazorpayQr -> refs.route.value = AppRoute.BookingDetailsTabs(
|
is AppRoute.RazorpayQr -> refs.route.value = AppRoute.BookingDetailsTabs(
|
||||||
currentRoute.propertyId,
|
currentRoute.propertyId,
|
||||||
currentRoute.bookingId,
|
currentRoute.bookingId,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import com.android.trisolarispms.ui.roomstay.ActiveRoomStaysScreen
|
|||||||
import com.android.trisolarispms.ui.roomstay.ManageRoomStaySelection
|
import com.android.trisolarispms.ui.roomstay.ManageRoomStaySelection
|
||||||
import com.android.trisolarispms.ui.roomstay.ManageRoomStayRatesScreen
|
import com.android.trisolarispms.ui.roomstay.ManageRoomStayRatesScreen
|
||||||
import com.android.trisolarispms.ui.roomstay.ManageRoomStaySelectScreen
|
import com.android.trisolarispms.ui.roomstay.ManageRoomStaySelectScreen
|
||||||
|
import com.android.trisolarispms.ui.settings.PropertySettingsScreen
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun renderStayFlowRoutes(
|
internal fun renderStayFlowRoutes(
|
||||||
@@ -70,13 +71,13 @@ internal fun renderStayFlowRoutes(
|
|||||||
},
|
},
|
||||||
showBack = !shouldBlockHomeBack(authz, state, currentRoute.propertyId),
|
showBack = !shouldBlockHomeBack(authz, state, currentRoute.propertyId),
|
||||||
onViewRooms = { refs.route.value = AppRoute.Rooms(currentRoute.propertyId) },
|
onViewRooms = { refs.route.value = AppRoute.Rooms(currentRoute.propertyId) },
|
||||||
|
onOpenSettings = { refs.route.value = AppRoute.PropertySettings(currentRoute.propertyId) },
|
||||||
onCreateBooking = { refs.route.value = AppRoute.CreateBooking(currentRoute.propertyId) },
|
onCreateBooking = { refs.route.value = AppRoute.CreateBooking(currentRoute.propertyId) },
|
||||||
canCreateBooking = authz.canCreateBookingFor(currentRoute.propertyId),
|
canCreateBooking = authz.canCreateBookingFor(currentRoute.propertyId),
|
||||||
showRazorpaySettings = authz.canManageRazorpaySettings(currentRoute.propertyId),
|
showRazorpaySettings = authz.canManageRazorpaySettings(currentRoute.propertyId),
|
||||||
onRazorpaySettings = { refs.route.value = AppRoute.RazorpaySettings(currentRoute.propertyId) },
|
onRazorpaySettings = { refs.route.value = AppRoute.RazorpaySettings(currentRoute.propertyId) },
|
||||||
showUserAdmin = authz.canManagePropertyUsers(currentRoute.propertyId),
|
showUserAdmin = authz.canManagePropertyUsers(currentRoute.propertyId),
|
||||||
onUserAdmin = { refs.route.value = AppRoute.PropertyUsers(currentRoute.propertyId) },
|
onUserAdmin = { refs.route.value = AppRoute.PropertyUsers(currentRoute.propertyId) },
|
||||||
onLogout = authViewModel::signOut,
|
|
||||||
onManageRoomStay = { booking ->
|
onManageRoomStay = { booking ->
|
||||||
val fromAt = booking.checkInAt?.takeIf { it.isNotBlank() }
|
val fromAt = booking.checkInAt?.takeIf { it.isNotBlank() }
|
||||||
?: booking.expectedCheckInAt.orEmpty()
|
?: booking.expectedCheckInAt.orEmpty()
|
||||||
@@ -111,6 +112,14 @@ internal fun renderStayFlowRoutes(
|
|||||||
onBack = { refs.openActiveRoomStays(currentRoute.propertyId) }
|
onBack = { refs.openActiveRoomStays(currentRoute.propertyId) }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
is AppRoute.PropertySettings -> PropertySettingsScreen(
|
||||||
|
propertyId = currentRoute.propertyId,
|
||||||
|
canManageCancellationPolicy = authz.canManageCancellationPolicy(currentRoute.propertyId),
|
||||||
|
canManageBillingPolicy = authz.canManageBillingPolicy(currentRoute.propertyId),
|
||||||
|
onBack = { refs.openActiveRoomStays(currentRoute.propertyId) },
|
||||||
|
onLogout = authViewModel::signOut
|
||||||
|
)
|
||||||
|
|
||||||
is AppRoute.RazorpayQr -> RazorpayQrScreen(
|
is AppRoute.RazorpayQr -> RazorpayQrScreen(
|
||||||
propertyId = currentRoute.propertyId,
|
propertyId = currentRoute.propertyId,
|
||||||
bookingId = currentRoute.bookingId,
|
bookingId = currentRoute.bookingId,
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import androidx.compose.material.icons.filled.ArrowBack
|
|||||||
import androidx.compose.material.icons.filled.People
|
import androidx.compose.material.icons.filled.People
|
||||||
import androidx.compose.material.icons.filled.MeetingRoom
|
import androidx.compose.material.icons.filled.MeetingRoom
|
||||||
import androidx.compose.material.icons.filled.Payment
|
import androidx.compose.material.icons.filled.Payment
|
||||||
import androidx.compose.material.icons.filled.MoreVert
|
import androidx.compose.material.icons.filled.Settings
|
||||||
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
|
||||||
@@ -32,8 +32,6 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.DropdownMenu
|
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.material3.LinearProgressIndicator
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
@@ -41,8 +39,6 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.LaunchedEffect
|
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.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
@@ -58,13 +54,13 @@ fun ActiveRoomStaysScreen(
|
|||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
showBack: Boolean,
|
showBack: Boolean,
|
||||||
onViewRooms: () -> Unit,
|
onViewRooms: () -> Unit,
|
||||||
|
onOpenSettings: () -> Unit,
|
||||||
onCreateBooking: () -> Unit,
|
onCreateBooking: () -> Unit,
|
||||||
canCreateBooking: Boolean,
|
canCreateBooking: Boolean,
|
||||||
showRazorpaySettings: Boolean,
|
showRazorpaySettings: Boolean,
|
||||||
onRazorpaySettings: () -> Unit,
|
onRazorpaySettings: () -> Unit,
|
||||||
showUserAdmin: Boolean,
|
showUserAdmin: Boolean,
|
||||||
onUserAdmin: () -> Unit,
|
onUserAdmin: () -> Unit,
|
||||||
onLogout: () -> Unit,
|
|
||||||
onManageRoomStay: (BookingListItem) -> Unit,
|
onManageRoomStay: (BookingListItem) -> Unit,
|
||||||
onViewBookingStays: (BookingListItem) -> Unit,
|
onViewBookingStays: (BookingListItem) -> Unit,
|
||||||
onOpenBookingDetails: (BookingListItem) -> Unit,
|
onOpenBookingDetails: (BookingListItem) -> Unit,
|
||||||
@@ -72,7 +68,6 @@ fun ActiveRoomStaysScreen(
|
|||||||
) {
|
) {
|
||||||
val state by viewModel.state.collectAsState()
|
val state by viewModel.state.collectAsState()
|
||||||
val selectedBooking = remember { mutableStateOf<BookingListItem?>(null) }
|
val selectedBooking = remember { mutableStateOf<BookingListItem?>(null) }
|
||||||
val menuExpanded = remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
LaunchedEffect(propertyId) {
|
LaunchedEffect(propertyId) {
|
||||||
viewModel.load(propertyId)
|
viewModel.load(propertyId)
|
||||||
@@ -93,6 +88,9 @@ fun ActiveRoomStaysScreen(
|
|||||||
IconButton(onClick = onViewRooms) {
|
IconButton(onClick = onViewRooms) {
|
||||||
Icon(Icons.Default.MeetingRoom, contentDescription = "Available Rooms")
|
Icon(Icons.Default.MeetingRoom, contentDescription = "Available Rooms")
|
||||||
}
|
}
|
||||||
|
IconButton(onClick = onOpenSettings) {
|
||||||
|
Icon(Icons.Default.Settings, contentDescription = "Settings")
|
||||||
|
}
|
||||||
if (showRazorpaySettings) {
|
if (showRazorpaySettings) {
|
||||||
IconButton(onClick = onRazorpaySettings) {
|
IconButton(onClick = onRazorpaySettings) {
|
||||||
Icon(Icons.Default.Payment, contentDescription = "Razorpay Settings")
|
Icon(Icons.Default.Payment, contentDescription = "Razorpay Settings")
|
||||||
@@ -103,21 +101,6 @@ fun ActiveRoomStaysScreen(
|
|||||||
Icon(Icons.Default.People, contentDescription = "Property Users")
|
Icon(Icons.Default.People, contentDescription = "Property Users")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
IconButton(onClick = { menuExpanded.value = true }) {
|
|
||||||
Icon(Icons.Default.MoreVert, contentDescription = "Menu")
|
|
||||||
}
|
|
||||||
DropdownMenu(
|
|
||||||
expanded = menuExpanded.value,
|
|
||||||
onDismissRequest = { menuExpanded.value = false }
|
|
||||||
) {
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text("Logout") },
|
|
||||||
onClick = {
|
|
||||||
menuExpanded.value = false
|
|
||||||
onLogout()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
colors = TopAppBarDefaults.topAppBarColors()
|
colors = TopAppBarDefaults.topAppBarColors()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ import coil.decode.SvgDecoder
|
|||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import com.android.trisolarispms.data.api.core.ApiConstants
|
import com.android.trisolarispms.data.api.core.ApiConstants
|
||||||
import com.android.trisolarispms.data.api.core.FirebaseAuthTokenProvider
|
import com.android.trisolarispms.data.api.core.FirebaseAuthTokenProvider
|
||||||
|
import com.android.trisolarispms.data.api.model.BookingBillingMode
|
||||||
import com.android.trisolarispms.data.api.model.BookingDetailsResponse
|
import com.android.trisolarispms.data.api.model.BookingDetailsResponse
|
||||||
import com.android.trisolarispms.ui.guestdocs.GuestDocumentsTab
|
import com.android.trisolarispms.ui.guestdocs.GuestDocumentsTab
|
||||||
import com.google.firebase.auth.FirebaseAuth
|
import com.google.firebase.auth.FirebaseAuth
|
||||||
@@ -264,6 +265,14 @@ private fun GuestInfoTabContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val billingMode = BookingBillingMode.from(details?.billingMode)
|
||||||
|
GuestDetailRow(label = "Billing Mode", value = details?.billingMode)
|
||||||
|
if (billingMode == BookingBillingMode.FULL_24H) {
|
||||||
|
GuestDetailRow(label = "Billing Window", value = "24-hour block")
|
||||||
|
} else {
|
||||||
|
GuestDetailRow(label = "Billing Check-in Time", value = details?.billingCheckinTime)
|
||||||
|
GuestDetailRow(label = "Billing Checkout Time", value = details?.billingCheckoutTime)
|
||||||
|
}
|
||||||
GuestDetailRow(
|
GuestDetailRow(
|
||||||
label = "Rooms Booked",
|
label = "Rooms Booked",
|
||||||
value = details?.roomNumbers?.takeIf { it.isNotEmpty() }?.joinToString(", ")
|
value = details?.roomNumbers?.takeIf { it.isNotEmpty() }?.joinToString(", ")
|
||||||
|
|||||||
@@ -0,0 +1,240 @@
|
|||||||
|
package com.android.trisolarispms.ui.settings
|
||||||
|
|
||||||
|
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.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.ExposedDropdownMenuBox
|
||||||
|
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
|
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.CancellationPenaltyMode
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
fun PropertySettingsScreen(
|
||||||
|
propertyId: String,
|
||||||
|
canManageCancellationPolicy: Boolean,
|
||||||
|
canManageBillingPolicy: Boolean,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
onLogout: () -> Unit,
|
||||||
|
viewModel: PropertySettingsViewModel = viewModel()
|
||||||
|
) {
|
||||||
|
val state by viewModel.state.collectAsState()
|
||||||
|
|
||||||
|
val canLoadPolicies = canManageCancellationPolicy || canManageBillingPolicy
|
||||||
|
LaunchedEffect(propertyId, canLoadPolicies) {
|
||||||
|
if (canLoadPolicies) {
|
||||||
|
viewModel.load(propertyId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text("Settings") },
|
||||||
|
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
|
||||||
|
) {
|
||||||
|
if (canManageCancellationPolicy) {
|
||||||
|
Text(
|
||||||
|
text = "Cancellation Policy",
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
text = "Configure how much to charge when booking is cancelled close to check-in.",
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
if (state.isLoading) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
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.freeDaysBeforeCheckinInput,
|
||||||
|
onValueChange = viewModel::onFreeDaysBeforeCheckinChange,
|
||||||
|
label = { Text("Free days before check-in") },
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
PenaltyModeDropdown(
|
||||||
|
value = state.penaltyMode,
|
||||||
|
enabled = true,
|
||||||
|
onSelect = viewModel::onPenaltyModeChange
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = { viewModel.save(propertyId) },
|
||||||
|
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 policy")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canManageBillingPolicy) {
|
||||||
|
Text(
|
||||||
|
text = "Billing Policy",
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
text = "Manage default billing check-in and checkout times in HH:mm format.",
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state.billingCheckinTimeInput,
|
||||||
|
onValueChange = viewModel::onBillingCheckinTimeChange,
|
||||||
|
label = { Text("Billing check-in time (HH:mm)") },
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
OutlinedTextField(
|
||||||
|
value = state.billingCheckoutTimeInput,
|
||||||
|
onValueChange = viewModel::onBillingCheckoutTimeChange,
|
||||||
|
label = { Text("Billing checkout time (HH:mm)") },
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Button(
|
||||||
|
onClick = { viewModel.saveBillingPolicy(propertyId) },
|
||||||
|
enabled = !state.isSavingBilling,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
if (state.isSavingBilling) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(16.dp)
|
||||||
|
.padding(end = 8.dp),
|
||||||
|
strokeWidth = 2.dp
|
||||||
|
)
|
||||||
|
Text("Saving...")
|
||||||
|
} else {
|
||||||
|
Text("Save billing policy")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = onLogout,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text("Logout")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
private fun PenaltyModeDropdown(
|
||||||
|
value: CancellationPenaltyMode,
|
||||||
|
enabled: Boolean,
|
||||||
|
onSelect: (CancellationPenaltyMode) -> Unit
|
||||||
|
) {
|
||||||
|
val expanded = remember { mutableStateOf(false) }
|
||||||
|
ExposedDropdownMenuBox(
|
||||||
|
expanded = expanded.value,
|
||||||
|
onExpandedChange = { if (enabled) expanded.value = !expanded.value }
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = value.name,
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true,
|
||||||
|
label = { Text("Penalty mode") },
|
||||||
|
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded.value) },
|
||||||
|
modifier = Modifier
|
||||||
|
.menuAnchor()
|
||||||
|
.fillMaxWidth()
|
||||||
|
)
|
||||||
|
ExposedDropdownMenu(
|
||||||
|
expanded = expanded.value,
|
||||||
|
onDismissRequest = { expanded.value = false }
|
||||||
|
) {
|
||||||
|
CancellationPenaltyMode.entries.forEach { mode ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(mode.name) },
|
||||||
|
onClick = {
|
||||||
|
expanded.value = false
|
||||||
|
onSelect(mode)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.android.trisolarispms.ui.settings
|
||||||
|
|
||||||
|
import com.android.trisolarispms.data.api.model.CancellationPenaltyMode
|
||||||
|
|
||||||
|
data class PropertySettingsState(
|
||||||
|
val freeDaysBeforeCheckinInput: String = "",
|
||||||
|
val penaltyMode: CancellationPenaltyMode = CancellationPenaltyMode.ONE_NIGHT,
|
||||||
|
val billingCheckinTimeInput: String = "",
|
||||||
|
val billingCheckoutTimeInput: String = "",
|
||||||
|
val isLoading: Boolean = false,
|
||||||
|
val isSaving: Boolean = false,
|
||||||
|
val isSavingBilling: Boolean = false,
|
||||||
|
val error: String? = null,
|
||||||
|
val message: String? = null
|
||||||
|
)
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
package com.android.trisolarispms.ui.settings
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.android.trisolarispms.data.api.core.ApiClient
|
||||||
|
import com.android.trisolarispms.data.api.model.BillingPolicyRequest
|
||||||
|
import com.android.trisolarispms.data.api.model.CancellationPenaltyMode
|
||||||
|
import com.android.trisolarispms.data.api.model.CancellationPolicyRequest
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class PropertySettingsViewModel : ViewModel() {
|
||||||
|
private val _state = MutableStateFlow(PropertySettingsState())
|
||||||
|
val state: StateFlow<PropertySettingsState> = _state
|
||||||
|
|
||||||
|
fun load(propertyId: String) {
|
||||||
|
if (propertyId.isBlank()) return
|
||||||
|
viewModelScope.launch {
|
||||||
|
_state.update { it.copy(isLoading = true, error = null, message = null) }
|
||||||
|
try {
|
||||||
|
val api = ApiClient.create()
|
||||||
|
val cancellationResponse = api.getCancellationPolicy(propertyId)
|
||||||
|
val billingResponse = api.getBillingPolicy(propertyId)
|
||||||
|
val cancellationBody = cancellationResponse.body()
|
||||||
|
val billingBody = billingResponse.body()
|
||||||
|
if (
|
||||||
|
cancellationResponse.isSuccessful &&
|
||||||
|
cancellationBody != null &&
|
||||||
|
billingResponse.isSuccessful &&
|
||||||
|
billingBody != null
|
||||||
|
) {
|
||||||
|
val mode = CancellationPenaltyMode.from(cancellationBody.penaltyMode) ?: CancellationPenaltyMode.ONE_NIGHT
|
||||||
|
val freeDays = cancellationBody.freeDaysBeforeCheckin ?: 0
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
freeDaysBeforeCheckinInput = freeDays.toString(),
|
||||||
|
penaltyMode = mode,
|
||||||
|
billingCheckinTimeInput = billingBody.billingCheckinTime.orEmpty(),
|
||||||
|
billingCheckoutTimeInput = billingBody.billingCheckoutTime.orEmpty(),
|
||||||
|
isLoading = false,
|
||||||
|
isSaving = false,
|
||||||
|
isSavingBilling = false,
|
||||||
|
error = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
isSaving = false,
|
||||||
|
isSavingBilling = false,
|
||||||
|
error = "Load failed: ${cancellationResponse.code()}/${billingResponse.code()}",
|
||||||
|
message = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
isSaving = false,
|
||||||
|
isSavingBilling = false,
|
||||||
|
error = e.localizedMessage ?: "Load failed",
|
||||||
|
message = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onFreeDaysBeforeCheckinChange(value: String) {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
freeDaysBeforeCheckinInput = value.filter { ch -> ch.isDigit() },
|
||||||
|
error = null,
|
||||||
|
message = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onPenaltyModeChange(value: CancellationPenaltyMode) {
|
||||||
|
_state.update { it.copy(penaltyMode = value, error = null, message = null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onBillingCheckinTimeChange(value: String) {
|
||||||
|
_state.update { it.copy(billingCheckinTimeInput = value, error = null, message = null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onBillingCheckoutTimeChange(value: String) {
|
||||||
|
_state.update { it.copy(billingCheckoutTimeInput = value, error = null, message = null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun save(propertyId: String) {
|
||||||
|
if (propertyId.isBlank()) return
|
||||||
|
val current = state.value
|
||||||
|
val freeDays = current.freeDaysBeforeCheckinInput.toIntOrNull()
|
||||||
|
if (freeDays == null) {
|
||||||
|
_state.update { it.copy(error = "Free days before check-in is required", message = null) }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
viewModelScope.launch {
|
||||||
|
_state.update { it.copy(isSaving = true, error = null, message = null) }
|
||||||
|
try {
|
||||||
|
val api = ApiClient.create()
|
||||||
|
val response = api.updateCancellationPolicy(
|
||||||
|
propertyId = propertyId,
|
||||||
|
body = CancellationPolicyRequest(
|
||||||
|
freeDaysBeforeCheckin = freeDays,
|
||||||
|
penaltyMode = current.penaltyMode
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
_state.update { it.copy(isSaving = false, error = null, 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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveBillingPolicy(propertyId: String) {
|
||||||
|
if (propertyId.isBlank()) return
|
||||||
|
val current = state.value
|
||||||
|
val checkin = current.billingCheckinTimeInput.trim()
|
||||||
|
val checkout = current.billingCheckoutTimeInput.trim()
|
||||||
|
if (!isValidHhMm(checkin)) {
|
||||||
|
_state.update { it.copy(error = "billingCheckinTime must be HH:mm", message = null) }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!isValidHhMm(checkout)) {
|
||||||
|
_state.update { it.copy(error = "billingCheckoutTime must be HH:mm", message = null) }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
viewModelScope.launch {
|
||||||
|
_state.update { it.copy(isSavingBilling = true, error = null, message = null) }
|
||||||
|
try {
|
||||||
|
val api = ApiClient.create()
|
||||||
|
val response = api.updateBillingPolicy(
|
||||||
|
propertyId = propertyId,
|
||||||
|
body = BillingPolicyRequest(
|
||||||
|
billingCheckinTime = checkin,
|
||||||
|
billingCheckoutTime = checkout
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
_state.update { it.copy(isSavingBilling = false, error = null, message = "Saved") }
|
||||||
|
} else {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isSavingBilling = false,
|
||||||
|
error = "Save failed: ${response.code()}",
|
||||||
|
message = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isSavingBilling = false,
|
||||||
|
error = e.localizedMessage ?: "Save failed",
|
||||||
|
message = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isValidHhMm(value: String): Boolean {
|
||||||
|
val regex = Regex("^([01]\\d|2[0-3]):[0-5]\\d$")
|
||||||
|
return regex.matches(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user