Compare commits

...

5 Commits

Author SHA1 Message Date
androidlover5842
8c790fbce0 improve auth policy 2026-02-02 06:21:18 +05:30
androidlover5842
f97834291d manage packages 2026-02-02 06:10:34 +05:30
androidlover5842
0f0db0dcf5 mainscreen: split codes 2026-02-02 06:02:38 +05:30
androidlover5842
99ce18a435 remove auth boilerplate 2026-02-02 05:39:20 +05:30
androidlover5842
342ff6a237 roles: fix permissions 2026-02-02 05:27:06 +05:30
75 changed files with 1171 additions and 918 deletions

View File

@@ -527,3 +527,59 @@ GET /properties/{propertyId}/bookings/{bookingId}/balance
## 7) Compose Notes
- Use `androidx.compose.foundation.text.KeyboardOptions` for keyboard options imports.
---
## 8) Engineering Structure & Anti-Boilerplate Rules
### Non-negotiable coding rules
- Never add duplicate business logic in multiple files.
- Never copy-paste permission checks, navigation decisions, mapping logic, or API call patterns.
- If similar logic appears 2+ times, extract shared function/class immediately.
- Prefer typed models/enums over raw strings for roles/status/flags.
- Keep files small and purpose-driven; split before a file becomes hard to scan.
### Required project structure (current baseline)
- `core/` -> cross-cutting business primitives/policies (e.g., auth policy, role enum).
- `data/api/core/` -> API client, constants, token providers, aggregated API service.
- `data/api/service/` -> Retrofit endpoint interfaces only.
- `data/api/model/` -> DTO/request/response models.
- `ui/navigation/` -> route model, navigation orchestrators, back-navigation rules.
- `ui/<feature>/` -> screen + state + viewmodel for that feature.
### How to implement future logic (mandatory workflow)
1. Define/extend domain type first (enum/data model/policy) instead of raw literals.
2. Add/extend API contract in `data/api/service` and models in `data/api/model`.
3. Add shared logic once (policy/helper/mapper) in `core` or feature-common layer.
4. Keep ViewModel thin: orchestrate calls, state, and errors only.
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).
7. Before finishing, remove any newly introduced duplication and compile-check.
### PR/refactor acceptance checklist
- No repeated role/permission checks across screens.
- No repeated model mapping blocks (extract mapper/helper).
- No giant god-file when it can be split by domain responsibility.
- Imports/packages follow the structure above.
- Build passes: `./gradlew :app:compileDebugKotlin`.
### Guest Documents Authorization (mandatory)
- View access: `ADMIN`, `MANAGER` (and super admin).
- Modify access (upload/delete): allowed only when booking status is `OPEN` or `CHECKED_IN`.
- For `CHECKED_OUT`, `CANCELLED`, `NO_SHOW`: documents are read-only.
- Never couple guest document permissions with Razorpay/settings permissions.
### Permission design guardrail
- Do not reuse one feature's permission gate for another unrelated feature.
- Add explicit policy methods in `core/auth/AuthzPolicy` for each feature capability.
### Refactor safety rule
- Any package/file movement must include import updates in same change.
- After refactor, compile check is mandatory: `./gradlew :app:compileDebugKotlin`.

View File

@@ -3,52 +3,15 @@ package com.android.trisolarispms
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.compose.BackHandler
import androidx.activity.enableEdgeToEdge
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.LaunchedEffect
import androidx.lifecycle.viewmodel.compose.viewModel
import com.android.trisolarispms.ui.AppRoute
import com.android.trisolarispms.ui.auth.AuthScreen
import com.android.trisolarispms.ui.auth.AuthViewModel
import com.android.trisolarispms.ui.auth.NameScreen
import com.android.trisolarispms.ui.auth.UnauthorizedScreen
import com.android.trisolarispms.ui.booking.BookingCreateScreen
import com.android.trisolarispms.ui.guest.GuestInfoScreen
import com.android.trisolarispms.ui.guest.GuestSignatureScreen
import com.android.trisolarispms.ui.booking.BookingExpectedDatesScreen
import com.android.trisolarispms.ui.roomstay.ManageRoomStayRatesScreen
import com.android.trisolarispms.ui.roomstay.ManageRoomStaySelectScreen
import com.android.trisolarispms.ui.roomstay.ManageRoomStaySelection
import com.android.trisolarispms.ui.roomstay.BookingRoomStaysScreen
import com.android.trisolarispms.ui.roomstay.BookingDetailsTabsScreen
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.room.RoomFormScreen
import com.android.trisolarispms.ui.room.RoomsScreen
import com.android.trisolarispms.ui.card.IssueTemporaryCardScreen
import com.android.trisolarispms.ui.card.CardInfoScreen
import com.android.trisolarispms.ui.roomimage.RoomImagesScreen
import com.android.trisolarispms.ui.roomimage.ImageTagsScreen
import com.android.trisolarispms.ui.roomimage.AddImageTagScreen
import com.android.trisolarispms.ui.roomimage.EditImageTagScreen
import com.android.trisolarispms.ui.roomstay.ActiveRoomStaysScreen
import com.android.trisolarispms.ui.razorpay.RazorpaySettingsScreen
import com.android.trisolarispms.ui.razorpay.RazorpayQrScreen
import com.android.trisolarispms.ui.users.PropertyUsersScreen
import com.android.trisolarispms.ui.users.PropertyAccessCodeScreen
import com.android.trisolarispms.ui.users.SuperAdminUserDirectoryScreen
import com.android.trisolarispms.ui.roomtype.AddAmenityScreen
import com.android.trisolarispms.ui.roomtype.AddRoomTypeScreen
import com.android.trisolarispms.ui.roomtype.AmenitiesScreen
import com.android.trisolarispms.ui.roomtype.EditAmenityScreen
import com.android.trisolarispms.ui.roomtype.EditRoomTypeScreen
import com.android.trisolarispms.ui.roomtype.RatePlanCalendarScreen
import com.android.trisolarispms.ui.roomtype.RoomTypesScreen
import com.android.trisolarispms.ui.navigation.MainRouteContent
import com.android.trisolarispms.ui.theme.TrisolarisPMSTheme
class MainActivity : ComponentActivity() {
@@ -60,709 +23,17 @@ class MainActivity : ComponentActivity() {
val authViewModel: AuthViewModel = viewModel()
val state by authViewModel.state.collectAsState()
if (state.unauthorized) {
UnauthorizedScreen(
when {
state.unauthorized -> UnauthorizedScreen(
message = state.error ?: "Not authorized. Contact admin.",
onSignOut = authViewModel::signOut
)
} else if (state.apiVerified && state.needsName) {
NameScreen(viewModel = authViewModel)
} else if (state.apiVerified) {
val route = remember { mutableStateOf<AppRoute>(AppRoute.Home) }
val refreshKey = remember { mutableStateOf(0) }
val selectedPropertyId = remember { mutableStateOf<String?>(null) }
val selectedPropertyName = remember { mutableStateOf<String?>(null) }
val selectedRoom = remember { mutableStateOf<com.android.trisolarispms.data.api.model.RoomDto?>(null) }
val selectedRoomType = remember { mutableStateOf<com.android.trisolarispms.data.api.model.RoomTypeDto?>(null) }
val selectedAmenity = remember { mutableStateOf<com.android.trisolarispms.data.api.model.AmenityDto?>(null) }
val selectedGuest = remember { mutableStateOf<com.android.trisolarispms.data.api.model.GuestDto?>(null) }
val selectedGuestPhone = remember { mutableStateOf<String?>(null) }
val selectedImageTag = remember { mutableStateOf<com.android.trisolarispms.data.api.model.RoomImageTagDto?>(null) }
val selectedManageRooms = remember { mutableStateOf<List<ManageRoomStaySelection>>(emptyList()) }
val roomFormKey = remember { mutableStateOf(0) }
val amenitiesReturnRoute = remember { mutableStateOf<AppRoute>(AppRoute.Home) }
val currentRoute = route.value
val singlePropertyId = state.propertyRoles.keys.firstOrNull()
val singlePropertyIsAdmin = singlePropertyId?.let {
state.propertyRoles[it]?.contains("ADMIN") == true
} ?: false
val canManageProperty: (String) -> Boolean = { propertyId ->
state.isSuperAdmin || (state.propertyRoles[propertyId]?.contains("ADMIN") == true)
}
val canIssueTemporaryCard: (String) -> Boolean = { propertyId ->
state.isSuperAdmin || state.propertyRoles[propertyId]?.any {
it == "ADMIN" || it == "MANAGER"
} == true
}
val canViewCardInfo: (String) -> Boolean = { propertyId ->
state.isSuperAdmin || state.propertyRoles[propertyId]?.any {
it == "ADMIN" || it == "MANAGER" || it == "STAFF"
} == true
}
val canManageRazorpaySettings: (String) -> Boolean = { propertyId ->
state.isSuperAdmin || state.propertyRoles[propertyId]?.contains("ADMIN") == true
}
val canDeleteCashPayment: (String) -> Boolean = { propertyId ->
state.isSuperAdmin || state.propertyRoles[propertyId]?.contains("ADMIN") == true
}
val canManagePropertyUsers: (String) -> Boolean = { propertyId ->
state.isSuperAdmin || state.propertyRoles[propertyId]?.contains("ADMIN") == true
}
val canCreateBookingFor: (String) -> Boolean = { propertyId ->
state.isSuperAdmin || state.propertyRoles[propertyId]?.any {
it == "ADMIN" || it == "MANAGER" || it == "STAFF"
} == true
}
val allowedAccessCodeRoles: (String) -> List<String> = { propertyId ->
if (state.isSuperAdmin) {
listOf("MANAGER", "STAFF", "AGENT")
} else {
val roles = state.propertyRoles[propertyId].orEmpty()
when {
roles.contains("ADMIN") -> listOf("MANAGER", "STAFF", "AGENT")
roles.contains("MANAGER") -> listOf("STAFF", "AGENT")
else -> emptyList()
}
}
}
BackHandler(enabled = currentRoute != AppRoute.Home) {
when (currentRoute) {
AppRoute.Home -> Unit
AppRoute.AddProperty -> route.value = AppRoute.Home
AppRoute.SuperAdminUsers -> route.value = AppRoute.Home
is AppRoute.ActiveRoomStays -> {
val isAdmin = state.propertyRoles[currentRoute.propertyId]?.contains("ADMIN") == true
val blockBack = !state.isSuperAdmin && !isAdmin && state.propertyRoles.size == 1
if (!blockBack) {
route.value = AppRoute.Home
}
}
is AppRoute.PropertyUsers -> route.value = AppRoute.ActiveRoomStays(
currentRoute.propertyId,
selectedPropertyName.value ?: "Property"
)
is AppRoute.PropertyAccessCode -> route.value = AppRoute.PropertyUsers(
currentRoute.propertyId
)
is AppRoute.Rooms -> route.value = AppRoute.ActiveRoomStays(
currentRoute.propertyId,
selectedPropertyName.value ?: "Property"
)
is AppRoute.AddRoom -> route.value = AppRoute.Rooms(currentRoute.propertyId)
is AppRoute.EditRoom -> route.value = AppRoute.Rooms(currentRoute.propertyId)
is AppRoute.RoomTypes -> route.value = AppRoute.Rooms(currentRoute.propertyId)
is AppRoute.AddRoomType -> route.value = AppRoute.RoomTypes(currentRoute.propertyId)
is AppRoute.EditRoomType -> route.value = AppRoute.RoomTypes(currentRoute.propertyId)
AppRoute.Amenities -> route.value = amenitiesReturnRoute.value
AppRoute.AddAmenity -> route.value = AppRoute.Amenities
is AppRoute.EditAmenity -> route.value = AppRoute.Amenities
AppRoute.ImageTags -> route.value = AppRoute.Home
AppRoute.AddImageTag -> route.value = AppRoute.ImageTags
is AppRoute.EditImageTag -> route.value = AppRoute.ImageTags
is AppRoute.RoomImages -> route.value = AppRoute.EditRoom(
currentRoute.propertyId,
currentRoute.roomId
)
is AppRoute.IssueTemporaryCard -> route.value = AppRoute.Rooms(currentRoute.propertyId)
is AppRoute.CardInfo -> route.value = AppRoute.Rooms(currentRoute.propertyId)
is AppRoute.RatePlanCalendar -> route.value = AppRoute.EditRoomType(
currentRoute.propertyId,
currentRoute.roomTypeId
)
is AppRoute.RazorpaySettings -> route.value = AppRoute.ActiveRoomStays(
currentRoute.propertyId,
selectedPropertyName.value ?: "Property"
)
is AppRoute.RazorpayQr -> route.value = AppRoute.BookingDetailsTabs(
currentRoute.propertyId,
currentRoute.bookingId,
null
)
is AppRoute.CreateBooking -> route.value = AppRoute.ActiveRoomStays(
currentRoute.propertyId,
selectedPropertyName.value ?: "Property"
)
is AppRoute.GuestInfo -> route.value = AppRoute.Home
is AppRoute.GuestSignature -> route.value = AppRoute.GuestInfo(
currentRoute.propertyId,
currentRoute.bookingId,
currentRoute.guestId
)
is AppRoute.ManageRoomStaySelect -> route.value = AppRoute.ActiveRoomStays(
currentRoute.propertyId,
selectedPropertyName.value ?: "Property"
)
is AppRoute.ManageRoomStayRates -> route.value = AppRoute.ManageRoomStaySelect(
currentRoute.propertyId,
currentRoute.bookingId,
currentRoute.fromAt,
currentRoute.toAt
)
is AppRoute.ManageRoomStaySelectFromBooking -> route.value = AppRoute.ActiveRoomStays(
currentRoute.propertyId,
selectedPropertyName.value ?: "Property"
)
is AppRoute.ManageRoomStayRatesFromBooking -> route.value = AppRoute.ManageRoomStaySelectFromBooking(
currentRoute.propertyId,
currentRoute.bookingId,
currentRoute.guestId,
currentRoute.fromAt,
currentRoute.toAt
)
is AppRoute.BookingRoomStays -> route.value = AppRoute.ActiveRoomStays(
currentRoute.propertyId,
selectedPropertyName.value ?: "Property"
)
is AppRoute.BookingExpectedDates -> route.value = AppRoute.ActiveRoomStays(
currentRoute.propertyId,
selectedPropertyName.value ?: "Property"
)
is AppRoute.BookingDetailsTabs -> route.value = AppRoute.ActiveRoomStays(
currentRoute.propertyId,
selectedPropertyName.value ?: "Property"
)
is AppRoute.BookingPayments -> route.value = AppRoute.BookingDetailsTabs(
currentRoute.propertyId,
currentRoute.bookingId,
null
)
}
}
if (currentRoute == AppRoute.Home &&
!state.isSuperAdmin &&
!singlePropertyIsAdmin &&
state.propertyRoles.size == 1 &&
singlePropertyId != null
) {
LaunchedEffect(singlePropertyId) {
selectedPropertyId.value = singlePropertyId
route.value = AppRoute.ActiveRoomStays(
singlePropertyId,
selectedPropertyName.value ?: "Property"
)
}
}
when (currentRoute) {
AppRoute.Home -> HomeScreen(
userId = state.userId,
userName = state.userName,
isSuperAdmin = state.isSuperAdmin,
onUserDirectory = { route.value = AppRoute.SuperAdminUsers },
onLogout = authViewModel::signOut,
onAddProperty = { route.value = AppRoute.AddProperty },
onAmenities = {
amenitiesReturnRoute.value = AppRoute.Home
route.value = AppRoute.Amenities
},
onImageTags = { route.value = AppRoute.ImageTags },
refreshKey = refreshKey.value,
selectedPropertyId = selectedPropertyId.value,
onSelectProperty = { id, name ->
selectedPropertyId.value = id
selectedPropertyName.value = name
route.value = AppRoute.ActiveRoomStays(id, name)
},
onRefreshProfile = authViewModel::refreshMe,
showJoinProperty = state.propertyRoles.isEmpty() && !state.isSuperAdmin,
onJoinPropertySuccess = { joinedPropertyId, joinedRoles ->
refreshKey.value++
authViewModel.refreshMe()
val isAdmin = joinedRoles.contains("ADMIN")
val shouldAutoOpen = !state.isSuperAdmin && !isAdmin && state.propertyRoles.size <= 1
if (shouldAutoOpen) {
route.value = AppRoute.ActiveRoomStays(
joinedPropertyId,
selectedPropertyName.value ?: "Property"
)
}
}
)
AppRoute.AddProperty -> AddPropertyScreen(
onBack = { route.value = AppRoute.Home },
onCreated = {
refreshKey.value++
route.value = AppRoute.Home
}
)
is AppRoute.CreateBooking -> BookingCreateScreen(
propertyId = currentRoute.propertyId,
onBack = {
route.value = AppRoute.ActiveRoomStays(
currentRoute.propertyId,
selectedPropertyName.value ?: "Property"
)
},
onCreated = { response, guest, phone ->
val bookingId = response.id.orEmpty()
val guestId = (guest?.id ?: response.guestId).orEmpty()
selectedGuest.value = guest
selectedGuestPhone.value = phone
if (bookingId.isNotBlank() && guestId.isNotBlank()) {
val fromAt = response.checkInAt?.takeIf { it.isNotBlank() }
?: response.expectedCheckInAt.orEmpty()
val toAt = response.expectedCheckOutAt?.takeIf { it.isNotBlank() }
route.value = AppRoute.ManageRoomStaySelectFromBooking(
propertyId = currentRoute.propertyId,
bookingId = bookingId,
guestId = guestId,
fromAt = fromAt,
toAt = toAt
)
} else {
route.value = AppRoute.Home
}
}
)
is AppRoute.GuestInfo -> GuestInfoScreen(
propertyId = currentRoute.propertyId,
guestId = currentRoute.guestId,
initialGuest = selectedGuest.value,
initialPhone = selectedGuestPhone.value,
onBack = { route.value = AppRoute.Home },
onSave = {
route.value = AppRoute.GuestSignature(
currentRoute.propertyId,
currentRoute.bookingId,
currentRoute.guestId
)
}
)
is AppRoute.GuestSignature -> GuestSignatureScreen(
propertyId = currentRoute.propertyId,
guestId = currentRoute.guestId,
onBack = {
route.value = AppRoute.GuestInfo(
currentRoute.propertyId,
currentRoute.bookingId,
currentRoute.guestId
)
},
onDone = {
route.value = AppRoute.ActiveRoomStays(
currentRoute.propertyId,
selectedPropertyName.value ?: "Property"
)
}
)
is AppRoute.ActiveRoomStays -> ActiveRoomStaysScreen(
propertyId = currentRoute.propertyId,
propertyName = currentRoute.propertyName,
onBack = {
val isAdmin = state.propertyRoles[currentRoute.propertyId]?.contains("ADMIN") == true
val blockBack = !state.isSuperAdmin && !isAdmin && state.propertyRoles.size == 1
if (!blockBack) {
route.value = AppRoute.Home
}
},
showBack = run {
val isAdmin = state.propertyRoles[currentRoute.propertyId]?.contains("ADMIN") == true
val blockBack = !state.isSuperAdmin && !isAdmin && state.propertyRoles.size == 1
!blockBack
},
onViewRooms = { route.value = AppRoute.Rooms(currentRoute.propertyId) },
onCreateBooking = { route.value = AppRoute.CreateBooking(currentRoute.propertyId) },
canCreateBooking = canCreateBookingFor(currentRoute.propertyId),
showRazorpaySettings = canManageRazorpaySettings(currentRoute.propertyId),
onRazorpaySettings = { route.value = AppRoute.RazorpaySettings(currentRoute.propertyId) },
showUserAdmin = canManagePropertyUsers(currentRoute.propertyId),
onUserAdmin = { route.value = AppRoute.PropertyUsers(currentRoute.propertyId) },
onLogout = authViewModel::signOut,
onManageRoomStay = { booking ->
val fromAt = booking.checkInAt?.takeIf { it.isNotBlank() }
?: booking.expectedCheckInAt.orEmpty()
val toAt = booking.expectedCheckOutAt?.takeIf { it.isNotBlank() }
?: booking.checkOutAt?.takeIf { it.isNotBlank() }
if (fromAt.isNotBlank()) {
route.value = AppRoute.ManageRoomStaySelect(
propertyId = currentRoute.propertyId,
bookingId = booking.id.orEmpty(),
fromAt = fromAt,
toAt = toAt
)
}
},
onViewBookingStays = { booking ->
route.value = AppRoute.BookingRoomStays(
propertyId = currentRoute.propertyId,
bookingId = booking.id.orEmpty()
)
},
onOpenBookingDetails = { booking ->
route.value = AppRoute.BookingDetailsTabs(
propertyId = currentRoute.propertyId,
bookingId = booking.id.orEmpty(),
guestId = booking.guestId
)
}
)
is AppRoute.RazorpaySettings -> RazorpaySettingsScreen(
propertyId = currentRoute.propertyId,
onBack = {
route.value = AppRoute.ActiveRoomStays(
currentRoute.propertyId,
selectedPropertyName.value ?: "Property"
)
}
)
is AppRoute.RazorpayQr -> RazorpayQrScreen(
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(
propertyId = currentRoute.propertyId,
bookingFromAt = currentRoute.fromAt,
bookingToAt = currentRoute.toAt,
onBack = {
route.value = AppRoute.ActiveRoomStays(
currentRoute.propertyId,
selectedPropertyName.value ?: "Property"
)
},
onNext = { rooms ->
selectedManageRooms.value = rooms
route.value = AppRoute.ManageRoomStayRates(
propertyId = currentRoute.propertyId,
bookingId = currentRoute.bookingId,
fromAt = currentRoute.fromAt,
toAt = currentRoute.toAt
)
}
)
is AppRoute.ManageRoomStaySelectFromBooking -> ManageRoomStaySelectScreen(
propertyId = currentRoute.propertyId,
bookingFromAt = currentRoute.fromAt,
bookingToAt = currentRoute.toAt,
onBack = {
route.value = AppRoute.ActiveRoomStays(
currentRoute.propertyId,
selectedPropertyName.value ?: "Property"
)
},
onNext = { rooms ->
selectedManageRooms.value = rooms
route.value = AppRoute.ManageRoomStayRatesFromBooking(
propertyId = currentRoute.propertyId,
bookingId = currentRoute.bookingId,
guestId = currentRoute.guestId,
fromAt = currentRoute.fromAt,
toAt = currentRoute.toAt
)
}
)
is AppRoute.ManageRoomStayRates -> ManageRoomStayRatesScreen(
propertyId = currentRoute.propertyId,
bookingId = currentRoute.bookingId,
checkInAt = currentRoute.fromAt,
checkOutAt = currentRoute.toAt,
selectedRooms = selectedManageRooms.value,
onBack = {
route.value = AppRoute.ManageRoomStaySelect(
currentRoute.propertyId,
currentRoute.bookingId,
currentRoute.fromAt,
currentRoute.toAt
)
},
onDone = {
route.value = AppRoute.ActiveRoomStays(
currentRoute.propertyId,
selectedPropertyName.value ?: "Property"
)
}
)
is AppRoute.ManageRoomStayRatesFromBooking -> ManageRoomStayRatesScreen(
propertyId = currentRoute.propertyId,
bookingId = currentRoute.bookingId,
checkInAt = currentRoute.fromAt,
checkOutAt = currentRoute.toAt,
selectedRooms = selectedManageRooms.value,
onBack = {
route.value = AppRoute.ManageRoomStaySelectFromBooking(
currentRoute.propertyId,
currentRoute.bookingId,
currentRoute.guestId,
currentRoute.fromAt,
currentRoute.toAt
)
},
onDone = {
route.value = AppRoute.GuestInfo(
currentRoute.propertyId,
currentRoute.bookingId,
currentRoute.guestId
)
}
)
is AppRoute.BookingRoomStays -> BookingRoomStaysScreen(
propertyId = currentRoute.propertyId,
bookingId = currentRoute.bookingId,
onBack = {
route.value = AppRoute.ActiveRoomStays(
currentRoute.propertyId,
selectedPropertyName.value ?: "Property"
)
}
)
is AppRoute.BookingExpectedDates -> BookingExpectedDatesScreen(
propertyId = currentRoute.propertyId,
bookingId = currentRoute.bookingId,
status = currentRoute.status,
expectedCheckInAt = currentRoute.expectedCheckInAt,
expectedCheckOutAt = currentRoute.expectedCheckOutAt,
onBack = {
route.value = AppRoute.ActiveRoomStays(
currentRoute.propertyId,
selectedPropertyName.value ?: "Property"
)
},
onDone = {
route.value = AppRoute.ActiveRoomStays(
currentRoute.propertyId,
selectedPropertyName.value ?: "Property"
)
}
)
is AppRoute.BookingDetailsTabs -> BookingDetailsTabsScreen(
propertyId = currentRoute.propertyId,
bookingId = currentRoute.bookingId,
guestId = currentRoute.guestId,
onBack = {
route.value = AppRoute.ActiveRoomStays(
currentRoute.propertyId,
selectedPropertyName.value ?: "Property"
)
},
onEditCheckout = { expectedCheckInAt, expectedCheckOutAt ->
route.value = AppRoute.BookingExpectedDates(
propertyId = currentRoute.propertyId,
bookingId = currentRoute.bookingId,
status = "CHECKED_IN",
expectedCheckInAt = expectedCheckInAt,
expectedCheckOutAt = expectedCheckOutAt
)
},
onEditSignature = { guestId ->
route.value = AppRoute.GuestSignature(
currentRoute.propertyId,
currentRoute.bookingId,
guestId
)
},
onOpenRazorpayQr = { pendingAmount, guestPhone ->
route.value = AppRoute.RazorpayQr(
propertyId = currentRoute.propertyId,
bookingId = currentRoute.bookingId,
pendingAmount = pendingAmount,
guestPhone = guestPhone
)
},
onOpenPayments = {
route.value = AppRoute.BookingPayments(
propertyId = currentRoute.propertyId,
bookingId = currentRoute.bookingId
)
},
canManageDocuments = canManageRazorpaySettings(currentRoute.propertyId)
)
is AppRoute.BookingPayments -> BookingPaymentsScreen(
propertyId = currentRoute.propertyId,
bookingId = currentRoute.bookingId,
canAddCash = canManageRazorpaySettings(currentRoute.propertyId),
canDeleteCash = canDeleteCashPayment(currentRoute.propertyId),
canRefund = canManageRazorpaySettings(currentRoute.propertyId),
onBack = {
route.value = AppRoute.BookingDetailsTabs(
currentRoute.propertyId,
currentRoute.bookingId,
null
)
}
)
AppRoute.SuperAdminUsers -> SuperAdminUserDirectoryScreen(
onBack = { route.value = AppRoute.Home }
)
is AppRoute.PropertyUsers -> PropertyUsersScreen(
propertyId = currentRoute.propertyId,
allowedRoleAssignments = when {
state.isSuperAdmin -> listOf("ADMIN", "MANAGER", "STAFF", "AGENT")
state.propertyRoles[currentRoute.propertyId]?.contains("ADMIN") == true ->
listOf("ADMIN", "MANAGER", "STAFF", "AGENT")
state.propertyRoles[currentRoute.propertyId]?.contains("MANAGER") == true ->
listOf("STAFF", "AGENT")
else -> emptyList()
},
canDisableAdmin = state.isSuperAdmin ||
state.propertyRoles[currentRoute.propertyId]?.contains("ADMIN") == true,
canDisableManager = state.propertyRoles[currentRoute.propertyId]?.contains("MANAGER") == true,
onBack = {
route.value = AppRoute.ActiveRoomStays(
currentRoute.propertyId,
selectedPropertyName.value ?: "Property"
)
},
onOpenAccessCode = {
route.value = AppRoute.PropertyAccessCode(currentRoute.propertyId)
}
)
is AppRoute.PropertyAccessCode -> PropertyAccessCodeScreen(
propertyId = currentRoute.propertyId,
allowedRoles = allowedAccessCodeRoles(currentRoute.propertyId),
onBack = { route.value = AppRoute.PropertyUsers(currentRoute.propertyId) }
)
is AppRoute.Rooms -> RoomsScreen(
propertyId = currentRoute.propertyId,
onBack = {
route.value = AppRoute.ActiveRoomStays(
currentRoute.propertyId,
selectedPropertyName.value ?: "Property"
)
},
onAddRoom = {
roomFormKey.value++
route.value = AppRoute.AddRoom(currentRoute.propertyId)
},
onViewRoomTypes = { route.value = AppRoute.RoomTypes(currentRoute.propertyId) },
onViewCardInfo = { route.value = AppRoute.CardInfo(currentRoute.propertyId) },
canManageRooms = canManageProperty(currentRoute.propertyId),
canViewCardInfo = canViewCardInfo(currentRoute.propertyId),
canIssueTemporaryCard = canIssueTemporaryCard(currentRoute.propertyId),
onEditRoom = {
selectedRoom.value = it
roomFormKey.value++
route.value = AppRoute.EditRoom(currentRoute.propertyId, it.id ?: "")
},
onIssueTemporaryCard = {
if (it.id != null && canIssueTemporaryCard(currentRoute.propertyId)) {
selectedRoom.value = it
route.value = AppRoute.IssueTemporaryCard(currentRoute.propertyId, it.id)
}
}
)
is AppRoute.RoomTypes -> RoomTypesScreen(
propertyId = currentRoute.propertyId,
onBack = { route.value = AppRoute.Rooms(currentRoute.propertyId) },
onAdd = { route.value = AppRoute.AddRoomType(currentRoute.propertyId) },
canManageRoomTypes = canManageProperty(currentRoute.propertyId),
onEdit = {
selectedRoomType.value = it
route.value = AppRoute.EditRoomType(currentRoute.propertyId, it.id ?: "")
}
)
is AppRoute.AddRoomType -> AddRoomTypeScreen(
propertyId = currentRoute.propertyId,
onBack = { route.value = AppRoute.RoomTypes(currentRoute.propertyId) },
onSave = { route.value = AppRoute.RoomTypes(currentRoute.propertyId) }
)
is AppRoute.EditRoomType -> EditRoomTypeScreen(
propertyId = currentRoute.propertyId,
roomType = selectedRoomType.value
?: com.android.trisolarispms.data.api.model.RoomTypeDto(id = currentRoute.roomTypeId, code = "", name = ""),
onBack = { route.value = AppRoute.RoomTypes(currentRoute.propertyId) },
onSave = { route.value = AppRoute.RoomTypes(currentRoute.propertyId) },
onOpenRatePlanCalendar = { ratePlanId, ratePlanCode ->
route.value = AppRoute.RatePlanCalendar(
currentRoute.propertyId,
currentRoute.roomTypeId,
ratePlanId,
ratePlanCode
)
}
)
AppRoute.Amenities -> AmenitiesScreen(
onBack = { route.value = amenitiesReturnRoute.value },
onAdd = { route.value = AppRoute.AddAmenity },
canManageAmenities = state.isSuperAdmin,
onEdit = {
selectedAmenity.value = it
route.value = AppRoute.EditAmenity(it.id ?: "")
}
)
AppRoute.AddAmenity -> AddAmenityScreen(
onBack = { route.value = AppRoute.Amenities },
onSave = { route.value = AppRoute.Amenities }
)
is AppRoute.EditAmenity -> EditAmenityScreen(
amenity = selectedAmenity.value
?: com.android.trisolarispms.data.api.model.AmenityDto(id = currentRoute.amenityId, name = ""),
onBack = { route.value = AppRoute.Amenities },
onSave = { route.value = AppRoute.Amenities }
)
AppRoute.ImageTags -> ImageTagsScreen(
onBack = { route.value = AppRoute.Home },
onAdd = { route.value = AppRoute.AddImageTag },
onEdit = {
selectedImageTag.value = it
route.value = AppRoute.EditImageTag(it.id ?: "")
}
)
AppRoute.AddImageTag -> AddImageTagScreen(
onBack = { route.value = AppRoute.ImageTags },
onSave = { route.value = AppRoute.ImageTags }
)
is AppRoute.EditImageTag -> EditImageTagScreen(
tag = selectedImageTag.value
?: com.android.trisolarispms.data.api.model.RoomImageTagDto(id = currentRoute.tagId, name = ""),
onBack = { route.value = AppRoute.ImageTags },
onSave = { route.value = AppRoute.ImageTags }
)
is AppRoute.AddRoom -> RoomFormScreen(
title = "Add Room",
propertyId = currentRoute.propertyId,
roomId = null,
roomData = null,
formKey = roomFormKey.value,
onBack = { route.value = AppRoute.Rooms(currentRoute.propertyId) },
onSave = { route.value = AppRoute.Rooms(currentRoute.propertyId) },
onViewImages = { }
)
is AppRoute.EditRoom -> RoomFormScreen(
title = "Modify Room",
propertyId = currentRoute.propertyId,
roomId = currentRoute.roomId,
roomData = selectedRoom.value,
formKey = roomFormKey.value,
onBack = { route.value = AppRoute.Rooms(currentRoute.propertyId) },
onSave = { route.value = AppRoute.Rooms(currentRoute.propertyId) },
onViewImages = { roomId ->
route.value = AppRoute.RoomImages(currentRoute.propertyId, roomId)
}
)
is AppRoute.IssueTemporaryCard -> IssueTemporaryCardScreen(
propertyId = currentRoute.propertyId,
roomId = currentRoute.roomId,
roomNumber = selectedRoom.value?.roomNumber?.toString(),
onBack = { route.value = AppRoute.Rooms(currentRoute.propertyId) }
)
is AppRoute.CardInfo -> CardInfoScreen(
propertyId = currentRoute.propertyId,
onBack = { route.value = AppRoute.Rooms(currentRoute.propertyId) }
)
is AppRoute.RatePlanCalendar -> RatePlanCalendarScreen(
propertyId = currentRoute.propertyId,
ratePlanId = currentRoute.ratePlanId,
ratePlanCode = currentRoute.ratePlanCode,
onBack = { route.value = AppRoute.EditRoomType(currentRoute.propertyId, currentRoute.roomTypeId) }
)
is AppRoute.RoomImages -> RoomImagesScreen(
propertyId = currentRoute.propertyId,
roomId = currentRoute.roomId,
onBack = { route.value = AppRoute.EditRoom(currentRoute.propertyId, currentRoute.roomId) }
)
}
} else {
AuthScreen(viewModel = authViewModel)
state.apiVerified && state.needsName -> NameScreen(viewModel = authViewModel)
state.apiVerified -> MainRouteContent(
state = state,
authViewModel = authViewModel
)
else -> AuthScreen(viewModel = authViewModel)
}
}
}

View File

@@ -0,0 +1,62 @@
package com.android.trisolarispms.core.auth
class AuthzPolicy(
private val isSuperAdmin: Boolean,
propertyRoles: Map<String, List<Role>>
) {
private val rolesByProperty: Map<String, Set<Role>> = propertyRoles.mapValues { it.value.toSet() }
fun isPropertyAdmin(propertyId: String): Boolean = hasRole(propertyId, Role.ADMIN)
fun canManageProperty(propertyId: String): Boolean = hasRole(propertyId, Role.ADMIN)
fun canIssueTemporaryCard(propertyId: String): Boolean =
hasAnyRole(propertyId, Role.ADMIN, Role.MANAGER)
fun canViewCardInfo(propertyId: String): Boolean =
hasAnyRole(propertyId, Role.ADMIN, Role.MANAGER, Role.STAFF)
fun canManageRazorpaySettings(propertyId: String): Boolean = hasRole(propertyId, Role.ADMIN)
fun canDeleteCashPayment(propertyId: String): Boolean = hasRole(propertyId, Role.ADMIN)
fun canAddBookingPayment(propertyId: String): Boolean =
hasAnyRole(propertyId, Role.ADMIN, Role.MANAGER, Role.STAFF)
fun canRefundBookingPayment(propertyId: String): Boolean =
hasAnyRole(propertyId, Role.ADMIN, Role.MANAGER)
fun canManageGuestDocuments(propertyId: String): Boolean =
hasAnyRole(propertyId, Role.ADMIN, Role.MANAGER)
fun canManagePropertyUsers(propertyId: String): Boolean = hasRole(propertyId, Role.ADMIN)
fun canCreateBookingFor(propertyId: String): Boolean =
hasAnyRole(propertyId, Role.ADMIN, Role.MANAGER, Role.STAFF)
fun canDisableAdmin(propertyId: String): Boolean = hasRole(propertyId, Role.ADMIN)
fun canDisableManager(propertyId: String): Boolean = hasRole(propertyId, Role.MANAGER)
fun allowedAccessCodeRoles(propertyId: String): List<Role> = when {
isSuperAdmin || hasRole(propertyId, Role.ADMIN) -> listOf(Role.MANAGER, Role.STAFF, Role.AGENT)
hasRole(propertyId, Role.MANAGER) -> listOf(Role.STAFF, Role.AGENT)
else -> emptyList()
}
fun allowedRoleAssignments(propertyId: String): List<Role> = when {
isSuperAdmin || hasRole(propertyId, Role.ADMIN) ->
listOf(Role.ADMIN, Role.MANAGER, Role.STAFF, Role.AGENT)
hasRole(propertyId, Role.MANAGER) -> listOf(Role.STAFF, Role.AGENT)
else -> emptyList()
}
fun shouldBlockBackToHome(propertyId: String, propertyCount: Int): Boolean =
!isSuperAdmin && !isPropertyAdmin(propertyId) && propertyCount == 1
private fun hasRole(propertyId: String, requiredRole: Role): Boolean =
isSuperAdmin || (rolesByProperty[propertyId]?.contains(requiredRole) == true)
private fun hasAnyRole(propertyId: String, vararg requiredRoles: Role): Boolean =
isSuperAdmin || (rolesByProperty[propertyId]?.any { role -> role in requiredRoles } == true)
}

View File

@@ -0,0 +1,27 @@
package com.android.trisolarispms.core.auth
import java.util.Locale
enum class Role {
ADMIN,
MANAGER,
STAFF,
HOUSEKEEPING,
FINANCE,
GUIDE,
SUPERVISOR,
AGENT;
companion object {
fun from(value: String?): Role? {
val normalized = value?.trim()?.uppercase(Locale.US) ?: return null
return entries.firstOrNull { it.name == normalized }
}
}
}
fun Collection<String>?.toRoles(): List<Role> = this.orEmpty().mapNotNull(Role::from)
fun Collection<String>?.toRoleSet(): Set<Role> = toRoles().toSet()
fun Collection<Role>.toRoleNameList(): List<String> = map { it.name }

View File

@@ -1,20 +0,0 @@
package com.android.trisolarispms.data.api
interface ApiService :
AuthApi,
PropertyApi,
RoomTypeApi,
RoomApi,
RoomImageApi,
ImageTagApi,
BookingApi,
RoomStayApi,
CardApi,
GuestApi,
GuestDocumentApi,
TransportApi,
InboundEmailApi,
AmenityApi,
RatePlanApi,
RazorpaySettingsApi,
UserAdminApi

View File

@@ -1,4 +1,4 @@
package com.android.trisolarispms.data.api
package com.android.trisolarispms.data.api.core
import com.google.firebase.auth.FirebaseAuth
import okhttp3.Authenticator

View File

@@ -1,4 +1,4 @@
package com.android.trisolarispms.data.api
package com.android.trisolarispms.data.api.core
object ApiConstants {
const val BASE_URL = "https://api.hoteltrisolaris.in/"

View File

@@ -0,0 +1,38 @@
package com.android.trisolarispms.data.api.core
import com.android.trisolarispms.data.api.service.AmenityApi
import com.android.trisolarispms.data.api.service.AuthApi
import com.android.trisolarispms.data.api.service.BookingApi
import com.android.trisolarispms.data.api.service.CardApi
import com.android.trisolarispms.data.api.service.GuestApi
import com.android.trisolarispms.data.api.service.GuestDocumentApi
import com.android.trisolarispms.data.api.service.ImageTagApi
import com.android.trisolarispms.data.api.service.InboundEmailApi
import com.android.trisolarispms.data.api.service.PropertyApi
import com.android.trisolarispms.data.api.service.RatePlanApi
import com.android.trisolarispms.data.api.service.RazorpaySettingsApi
import com.android.trisolarispms.data.api.service.RoomApi
import com.android.trisolarispms.data.api.service.RoomImageApi
import com.android.trisolarispms.data.api.service.RoomStayApi
import com.android.trisolarispms.data.api.service.RoomTypeApi
import com.android.trisolarispms.data.api.service.TransportApi
import com.android.trisolarispms.data.api.service.UserAdminApi
interface ApiService :
AuthApi,
PropertyApi,
RoomTypeApi,
RoomApi,
RoomImageApi,
ImageTagApi,
BookingApi,
RoomStayApi,
CardApi,
GuestApi,
GuestDocumentApi,
TransportApi,
InboundEmailApi,
AmenityApi,
RatePlanApi,
RazorpaySettingsApi,
UserAdminApi

View File

@@ -1,4 +1,4 @@
package com.android.trisolarispms.data.api
package com.android.trisolarispms.data.api.core
interface AuthTokenProvider {
suspend fun token(forceRefresh: Boolean = false): String?

View File

@@ -1,4 +1,4 @@
package com.android.trisolarispms.data.api
package com.android.trisolarispms.data.api.core
import com.google.firebase.auth.FirebaseAuth
import kotlinx.coroutines.tasks.await

View File

@@ -1,4 +1,4 @@
package com.android.trisolarispms.data.api
package com.android.trisolarispms.data.api.service
import com.android.trisolarispms.data.api.model.AmenityCreateRequest
import com.android.trisolarispms.data.api.model.AmenityDto

View File

@@ -1,4 +1,4 @@
package com.android.trisolarispms.data.api
package com.android.trisolarispms.data.api.service
import com.android.trisolarispms.data.api.model.AuthVerifyResponse
import com.android.trisolarispms.data.api.model.AuthMeUpdateRequest

View File

@@ -1,4 +1,4 @@
package com.android.trisolarispms.data.api
package com.android.trisolarispms.data.api.service
import com.android.trisolarispms.data.api.model.ActionResponse
import com.android.trisolarispms.data.api.model.BookingCancelRequest

View File

@@ -1,4 +1,4 @@
package com.android.trisolarispms.data.api
package com.android.trisolarispms.data.api.service
import com.android.trisolarispms.data.api.model.CardPrepareRequest
import com.android.trisolarispms.data.api.model.CardPrepareResponse

View File

@@ -1,4 +1,4 @@
package com.android.trisolarispms.data.api
package com.android.trisolarispms.data.api.service
import com.android.trisolarispms.data.api.model.GuestDto
import com.android.trisolarispms.data.api.model.GuestCreateRequest

View File

@@ -1,4 +1,4 @@
package com.android.trisolarispms.data.api
package com.android.trisolarispms.data.api.service
import com.android.trisolarispms.data.api.model.GuestDocumentDto
import okhttp3.MultipartBody

View File

@@ -1,4 +1,4 @@
package com.android.trisolarispms.data.api
package com.android.trisolarispms.data.api.service
import com.android.trisolarispms.data.api.model.RoomImageTagDto
import retrofit2.Response

View File

@@ -1,4 +1,4 @@
package com.android.trisolarispms.data.api
package com.android.trisolarispms.data.api.service
import com.android.trisolarispms.data.api.model.ActionResponse
import okhttp3.MultipartBody

View File

@@ -1,4 +1,4 @@
package com.android.trisolarispms.data.api
package com.android.trisolarispms.data.api.service
import com.android.trisolarispms.data.api.model.ActionResponse
import com.android.trisolarispms.data.api.model.PropertyCreateRequest

View File

@@ -1,4 +1,4 @@
package com.android.trisolarispms.data.api
package com.android.trisolarispms.data.api.service
import com.android.trisolarispms.data.api.model.RatePlanCalendarEntry
import com.android.trisolarispms.data.api.model.RatePlanCalendarUpsertRequest

View File

@@ -1,4 +1,4 @@
package com.android.trisolarispms.data.api
package com.android.trisolarispms.data.api.service
import com.android.trisolarispms.data.api.model.RazorpaySettingsRequest
import com.android.trisolarispms.data.api.model.RazorpaySettingsResponse

View File

@@ -1,4 +1,4 @@
package com.android.trisolarispms.data.api
package com.android.trisolarispms.data.api.service
import com.android.trisolarispms.data.api.model.RoomAvailabilityRangeResponse
import com.android.trisolarispms.data.api.model.RoomAvailabilityResponse

View File

@@ -1,4 +1,4 @@
package com.android.trisolarispms.data.api
package com.android.trisolarispms.data.api.service
import com.android.trisolarispms.data.api.model.ImageDto
import okhttp3.MultipartBody

View File

@@ -1,4 +1,4 @@
package com.android.trisolarispms.data.api
package com.android.trisolarispms.data.api.service
import com.android.trisolarispms.data.api.model.RoomChangeRequest
import com.android.trisolarispms.data.api.model.RoomChangeResponse

View File

@@ -1,4 +1,4 @@
package com.android.trisolarispms.data.api
package com.android.trisolarispms.data.api.service
import com.android.trisolarispms.data.api.model.ActionResponse
import com.android.trisolarispms.data.api.model.RoomTypeCreateRequest

View File

@@ -1,4 +1,4 @@
package com.android.trisolarispms.data.api
package com.android.trisolarispms.data.api.service
import com.android.trisolarispms.data.api.model.TransportModeDto
import retrofit2.Response

View File

@@ -1,4 +1,4 @@
package com.android.trisolarispms.data.api
package com.android.trisolarispms.data.api.service
import com.android.trisolarispms.data.api.model.AppUserSummaryResponse
import com.android.trisolarispms.data.api.model.PropertyAccessCodeCreateRequest

View File

@@ -1,5 +1,7 @@
package com.android.trisolarispms.ui.auth
import com.android.trisolarispms.core.auth.Role
data class AuthUiState(
val countryCode: String = "+91",
val phoneCountryCode: String = "IN",
@@ -19,5 +21,5 @@ data class AuthUiState(
val nameInput: String = "",
val needsName: Boolean = false,
val unauthorized: Boolean = false,
val propertyRoles: Map<String, List<String>> = emptyMap()
val propertyRoles: Map<String, List<Role>> = emptyMap()
)

View File

@@ -3,7 +3,9 @@ package com.android.trisolarispms.ui.auth
import androidx.activity.ComponentActivity
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.ApiClient
import com.android.trisolarispms.core.auth.Role
import com.android.trisolarispms.core.auth.toRoles
import com.android.trisolarispms.data.api.core.ApiClient
import com.google.firebase.FirebaseException
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.PhoneAuthCredential
@@ -208,50 +210,25 @@ class AuthViewModel(
response: retrofit2.Response<com.android.trisolarispms.data.api.model.AuthVerifyResponse>
) {
val body = response.body()
val status = body?.status
val userName = body?.user?.name
val isSuperAdmin = body?.user?.superAdmin == true
val propertyRoles = body?.properties
.orEmpty()
.mapNotNull { entry ->
val id = entry.propertyId
id?.let { it to entry.roles.orEmpty() }
id?.let { it to entry.roles.toRoles() }
}
.toMap()
when {
response.isSuccessful && (body?.status == "OK" || body?.status == "SUPER_ADMIN") -> {
_state.update {
it.copy(
isLoading = false,
userId = userId,
apiVerified = true,
needsName = userName.isNullOrBlank(),
nameInput = userName ?: "",
userName = userName,
isSuperAdmin = isSuperAdmin,
noProperties = body?.status == "NO_PROPERTIES",
unauthorized = false,
propertyRoles = propertyRoles,
error = null
)
}
}
response.isSuccessful && body?.status == "NO_PROPERTIES" -> {
_state.update {
it.copy(
isLoading = false,
userId = userId,
apiVerified = true,
needsName = userName.isNullOrBlank(),
nameInput = userName ?: "",
userName = userName,
isSuperAdmin = isSuperAdmin,
noProperties = true,
unauthorized = false,
propertyRoles = propertyRoles,
error = null
)
}
}
response.isSuccessful && (status == "OK" || status == "SUPER_ADMIN" || status == "NO_PROPERTIES") ->
setVerifiedState(
userId = userId,
userName = userName,
isSuperAdmin = isSuperAdmin,
propertyRoles = propertyRoles,
noProperties = status == "NO_PROPERTIES"
)
response.code() == 401 -> {
_state.update {
it.copy(
@@ -281,6 +258,30 @@ class AuthViewModel(
}
}
private fun setVerifiedState(
userId: String?,
userName: String?,
isSuperAdmin: Boolean,
propertyRoles: Map<String, List<Role>>,
noProperties: Boolean
) {
_state.update {
it.copy(
isLoading = false,
userId = userId,
apiVerified = true,
needsName = userName.isNullOrBlank(),
nameInput = userName ?: "",
userName = userName,
isSuperAdmin = isSuperAdmin,
noProperties = noProperties,
unauthorized = false,
propertyRoles = propertyRoles,
error = null
)
}
}
fun signOut() {
auth.signOut()
_state.update { AuthUiState() }
@@ -335,5 +336,4 @@ class AuthViewModel(
}
}
}
}

View File

@@ -2,7 +2,7 @@ package com.android.trisolarispms.ui.booking
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.ApiClient
import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.data.api.model.BookingCreateRequest
import com.android.trisolarispms.data.api.model.BookingCreateResponse
import com.android.trisolarispms.data.api.model.GuestDto

View File

@@ -37,7 +37,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.graphics.Color
import kotlinx.coroutines.launch
import androidx.compose.runtime.rememberCoroutineScope
import com.android.trisolarispms.data.api.ApiClient
import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.data.api.model.BookingExpectedDatesRequest
import com.kizitonwose.calendar.compose.HorizontalCalendar
import com.kizitonwose.calendar.compose.rememberCalendarState

View File

@@ -38,7 +38,7 @@ import com.airbnb.lottie.compose.LottieCompositionSpec
import com.airbnb.lottie.compose.LottieConstants
import com.airbnb.lottie.compose.animateLottieCompositionAsState
import com.airbnb.lottie.compose.rememberLottieComposition
import com.android.trisolarispms.data.api.ApiClient
import com.android.trisolarispms.data.api.core.ApiClient
import java.util.Calendar
import java.util.Date
import kotlinx.coroutines.launch

View File

@@ -5,7 +5,7 @@ import android.nfc.tech.MifareClassic
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.ApiClient
import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.data.api.model.CardPrepareRequest
import com.android.trisolarispms.data.api.model.IssueCardRequest
import kotlinx.coroutines.Dispatchers

View File

@@ -2,7 +2,7 @@ package com.android.trisolarispms.ui.guest
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.ApiClient
import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.data.api.model.GuestDto
import com.android.trisolarispms.data.api.model.GuestUpdateRequest
import kotlinx.coroutines.flow.MutableStateFlow

View File

@@ -2,7 +2,7 @@ package com.android.trisolarispms.ui.guest
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.ApiClient
import com.android.trisolarispms.data.api.core.ApiClient
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update

View File

@@ -48,8 +48,8 @@ import coil.ImageLoader
import coil.compose.SubcomposeAsyncImage
import coil.decode.SvgDecoder
import coil.request.ImageRequest
import com.android.trisolarispms.data.api.ApiConstants
import com.android.trisolarispms.data.api.FirebaseAuthTokenProvider
import com.android.trisolarispms.data.api.core.ApiConstants
import com.android.trisolarispms.data.api.core.FirebaseAuthTokenProvider
import com.android.trisolarispms.data.api.model.GuestDocumentDto
import com.google.firebase.auth.FirebaseAuth
import okhttp3.Interceptor
@@ -71,6 +71,7 @@ fun GuestDocumentsTab(
guestId: String,
bookingId: String,
canManageDocuments: Boolean,
canModifyDocuments: Boolean,
viewModel: GuestDocumentsViewModel = viewModel(key = "guestDocs:$propertyId:$guestId")
) {
val state by viewModel.state.collectAsState()
@@ -164,6 +165,13 @@ fun GuestDocumentsTab(
Text(text = "You don't have access to view documents.")
return@Column
}
if (!canModifyDocuments) {
Text(
text = "Read-only: documents can be modified only when booking is OPEN or CHECKED_IN.",
style = MaterialTheme.typography.bodySmall
)
Spacer(modifier = Modifier.height(8.dp))
}
if (!state.isLoading && state.documents.isEmpty()) {
Text(text = "No documents yet")
}
@@ -190,7 +198,7 @@ fun GuestDocumentsTab(
guestId = guestId,
doc = doc,
imageLoader = imageLoader,
canDelete = canManageDocuments,
canDelete = canModifyDocuments,
onDelete = { documentId ->
viewModel.deleteDocument(propertyId, guestId, documentId)
}
@@ -199,7 +207,7 @@ fun GuestDocumentsTab(
}
}
if (canManageDocuments) {
if (canModifyDocuments) {
FloatingActionButton(
onClick = { showPicker.value = true },
modifier = Modifier
@@ -214,7 +222,7 @@ fun GuestDocumentsTab(
}
}
if (showPicker.value) {
if (showPicker.value && canModifyDocuments) {
AlertDialog(
onDismissRequest = { showPicker.value = false },
title = { Text("Add document") },

View File

@@ -5,8 +5,8 @@ import android.content.Context
import android.net.Uri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.ApiClient
import com.android.trisolarispms.data.api.ApiConstants
import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.data.api.core.ApiConstants
import com.android.trisolarispms.data.api.model.GuestDocumentDto
import com.google.gson.Gson
import kotlinx.coroutines.flow.MutableStateFlow

View File

@@ -2,7 +2,9 @@ package com.android.trisolarispms.ui.home
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.ApiClient
import com.android.trisolarispms.core.auth.Role
import com.android.trisolarispms.core.auth.toRoles
import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.data.api.model.PropertyAccessCodeJoinRequest
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -14,7 +16,7 @@ data class HomeJoinPropertyState(
val error: String? = null,
val message: String? = null,
val joinedPropertyId: String? = null,
val joinedRoles: List<String> = emptyList()
val joinedRoles: List<Role> = emptyList()
)
class HomeJoinPropertyViewModel : ViewModel() {
@@ -49,7 +51,7 @@ class HomeJoinPropertyViewModel : ViewModel() {
message = "Joined property",
error = null,
joinedPropertyId = body.propertyId ?: trimmedPropertyId,
joinedRoles = body.roles
joinedRoles = body.roles.toRoles()
)
}
} else {

View File

@@ -33,6 +33,7 @@ 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.core.auth.Role
import com.android.trisolarispms.ui.property.PropertyListViewModel
import androidx.compose.foundation.text.KeyboardOptions
@@ -52,7 +53,7 @@ fun HomeScreen(
onSelectProperty: (String, String) -> Unit,
onRefreshProfile: () -> Unit,
showJoinProperty: Boolean,
onJoinPropertySuccess: (String, List<String>) -> Unit,
onJoinPropertySuccess: (String, List<Role>) -> Unit,
viewModel: PropertyListViewModel = viewModel(),
joinViewModel: HomeJoinPropertyViewModel = viewModel()
) {

View File

@@ -1,4 +1,4 @@
package com.android.trisolarispms.ui
package com.android.trisolarispms.ui.navigation
sealed interface AppRoute {
data object Home : AppRoute

View File

@@ -0,0 +1,82 @@
package com.android.trisolarispms.ui.navigation
import com.android.trisolarispms.core.auth.AuthzPolicy
import com.android.trisolarispms.ui.navigation.AppRoute
internal fun handleBackNavigation(
refs: MainUiRefs,
authz: AuthzPolicy,
propertyCount: Int
) {
when (val currentRoute = refs.currentRoute) {
AppRoute.Home -> Unit
AppRoute.AddProperty -> refs.route.value = AppRoute.Home
AppRoute.SuperAdminUsers -> refs.route.value = AppRoute.Home
is AppRoute.ActiveRoomStays -> {
val blockBack = authz.shouldBlockBackToHome(
propertyId = currentRoute.propertyId,
propertyCount = propertyCount
)
if (!blockBack) {
refs.route.value = AppRoute.Home
}
}
is AppRoute.PropertyUsers -> refs.openActiveRoomStays(currentRoute.propertyId)
is AppRoute.PropertyAccessCode -> refs.route.value = AppRoute.PropertyUsers(currentRoute.propertyId)
is AppRoute.Rooms -> refs.openActiveRoomStays(currentRoute.propertyId)
is AppRoute.AddRoom -> refs.route.value = AppRoute.Rooms(currentRoute.propertyId)
is AppRoute.EditRoom -> refs.route.value = AppRoute.Rooms(currentRoute.propertyId)
is AppRoute.RoomTypes -> refs.route.value = AppRoute.Rooms(currentRoute.propertyId)
is AppRoute.AddRoomType -> refs.route.value = AppRoute.RoomTypes(currentRoute.propertyId)
is AppRoute.EditRoomType -> refs.route.value = AppRoute.RoomTypes(currentRoute.propertyId)
AppRoute.Amenities -> refs.route.value = refs.amenitiesReturnRoute.value
AppRoute.AddAmenity -> refs.route.value = AppRoute.Amenities
is AppRoute.EditAmenity -> refs.route.value = AppRoute.Amenities
AppRoute.ImageTags -> refs.route.value = AppRoute.Home
AppRoute.AddImageTag -> refs.route.value = AppRoute.ImageTags
is AppRoute.EditImageTag -> refs.route.value = AppRoute.ImageTags
is AppRoute.RoomImages -> refs.route.value = AppRoute.EditRoom(currentRoute.propertyId, currentRoute.roomId)
is AppRoute.IssueTemporaryCard -> refs.route.value = AppRoute.Rooms(currentRoute.propertyId)
is AppRoute.CardInfo -> refs.route.value = AppRoute.Rooms(currentRoute.propertyId)
is AppRoute.RatePlanCalendar -> refs.route.value = AppRoute.EditRoomType(
currentRoute.propertyId,
currentRoute.roomTypeId
)
is AppRoute.RazorpaySettings -> refs.openActiveRoomStays(currentRoute.propertyId)
is AppRoute.RazorpayQr -> refs.route.value = AppRoute.BookingDetailsTabs(
currentRoute.propertyId,
currentRoute.bookingId,
null
)
is AppRoute.CreateBooking -> refs.openActiveRoomStays(currentRoute.propertyId)
is AppRoute.GuestInfo -> refs.route.value = AppRoute.Home
is AppRoute.GuestSignature -> refs.route.value = AppRoute.GuestInfo(
currentRoute.propertyId,
currentRoute.bookingId,
currentRoute.guestId
)
is AppRoute.ManageRoomStaySelect -> refs.openActiveRoomStays(currentRoute.propertyId)
is AppRoute.ManageRoomStayRates -> refs.route.value = AppRoute.ManageRoomStaySelect(
currentRoute.propertyId,
currentRoute.bookingId,
currentRoute.fromAt,
currentRoute.toAt
)
is AppRoute.ManageRoomStaySelectFromBooking -> refs.openActiveRoomStays(currentRoute.propertyId)
is AppRoute.ManageRoomStayRatesFromBooking -> refs.route.value = AppRoute.ManageRoomStaySelectFromBooking(
currentRoute.propertyId,
currentRoute.bookingId,
currentRoute.guestId,
currentRoute.fromAt,
currentRoute.toAt
)
is AppRoute.BookingRoomStays -> refs.openActiveRoomStays(currentRoute.propertyId)
is AppRoute.BookingExpectedDates -> refs.openActiveRoomStays(currentRoute.propertyId)
is AppRoute.BookingDetailsTabs -> refs.openActiveRoomStays(currentRoute.propertyId)
is AppRoute.BookingPayments -> refs.route.value = AppRoute.BookingDetailsTabs(
currentRoute.propertyId,
currentRoute.bookingId,
null
)
}
}

View File

@@ -0,0 +1,98 @@
package com.android.trisolarispms.ui.navigation
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import com.android.trisolarispms.core.auth.AuthzPolicy
import com.android.trisolarispms.data.api.model.AmenityDto
import com.android.trisolarispms.data.api.model.GuestDto
import com.android.trisolarispms.data.api.model.RoomDto
import com.android.trisolarispms.data.api.model.RoomImageTagDto
import com.android.trisolarispms.data.api.model.RoomTypeDto
import com.android.trisolarispms.ui.navigation.AppRoute
import com.android.trisolarispms.ui.auth.AuthUiState
import com.android.trisolarispms.ui.auth.AuthViewModel
import com.android.trisolarispms.ui.roomstay.ManageRoomStaySelection
@Composable
internal fun MainRouteContent(
state: AuthUiState,
authViewModel: AuthViewModel
) {
val refs = remember {
MainUiRefs(
route = mutableStateOf<AppRoute>(AppRoute.Home),
refreshKey = mutableStateOf(0),
selectedPropertyId = mutableStateOf<String?>(null),
selectedPropertyName = mutableStateOf<String?>(null),
selectedRoom = mutableStateOf<RoomDto?>(null),
selectedRoomType = mutableStateOf<RoomTypeDto?>(null),
selectedAmenity = mutableStateOf<AmenityDto?>(null),
selectedGuest = mutableStateOf<GuestDto?>(null),
selectedGuestPhone = mutableStateOf<String?>(null),
selectedImageTag = mutableStateOf<RoomImageTagDto?>(null),
selectedManageRooms = mutableStateOf<List<ManageRoomStaySelection>>(emptyList()),
roomFormKey = mutableStateOf(0),
amenitiesReturnRoute = mutableStateOf<AppRoute>(AppRoute.Home)
)
}
val authz = remember(state.isSuperAdmin, state.propertyRoles) {
AuthzPolicy(
isSuperAdmin = state.isSuperAdmin,
propertyRoles = state.propertyRoles
)
}
val singlePropertyId = state.propertyRoles.keys.firstOrNull()
val singlePropertyIsAdmin = singlePropertyId?.let(authz::isPropertyAdmin) ?: false
BackHandler(enabled = refs.currentRoute != AppRoute.Home) {
handleBackNavigation(
refs = refs,
authz = authz,
propertyCount = state.propertyRoles.size
)
}
if (
refs.currentRoute == AppRoute.Home &&
!state.isSuperAdmin &&
!singlePropertyIsAdmin &&
state.propertyRoles.size == 1 &&
singlePropertyId != null
) {
LaunchedEffect(singlePropertyId) {
refs.selectedPropertyId.value = singlePropertyId
refs.openActiveRoomStays(singlePropertyId)
}
}
when {
renderHomeGuestRoutes(
refs = refs,
state = state,
authViewModel = authViewModel
) -> Unit
renderStayFlowRoutes(
refs = refs,
state = state,
authViewModel = authViewModel,
authz = authz
) -> Unit
renderBookingRoutes(
refs = refs,
authz = authz
) -> Unit
renderManagementRoutes(
refs = refs,
stateIsSuperAdmin = state.isSuperAdmin,
authz = authz
) -> Unit
}
}

View File

@@ -0,0 +1,89 @@
package com.android.trisolarispms.ui.navigation
import androidx.compose.runtime.Composable
import com.android.trisolarispms.core.auth.AuthzPolicy
import com.android.trisolarispms.ui.navigation.AppRoute
import com.android.trisolarispms.ui.booking.BookingExpectedDatesScreen
import com.android.trisolarispms.ui.payment.BookingPaymentsScreen
import com.android.trisolarispms.ui.roomstay.BookingDetailsTabsScreen
import com.android.trisolarispms.ui.roomstay.BookingRoomStaysScreen
@Composable
internal fun renderBookingRoutes(
refs: MainUiRefs,
authz: AuthzPolicy
): Boolean {
when (val currentRoute = refs.currentRoute) {
is AppRoute.BookingRoomStays -> BookingRoomStaysScreen(
propertyId = currentRoute.propertyId,
bookingId = currentRoute.bookingId,
onBack = { refs.openActiveRoomStays(currentRoute.propertyId) }
)
is AppRoute.BookingExpectedDates -> BookingExpectedDatesScreen(
propertyId = currentRoute.propertyId,
bookingId = currentRoute.bookingId,
status = currentRoute.status,
expectedCheckInAt = currentRoute.expectedCheckInAt,
expectedCheckOutAt = currentRoute.expectedCheckOutAt,
onBack = { refs.openActiveRoomStays(currentRoute.propertyId) },
onDone = { refs.openActiveRoomStays(currentRoute.propertyId) }
)
is AppRoute.BookingDetailsTabs -> BookingDetailsTabsScreen(
propertyId = currentRoute.propertyId,
bookingId = currentRoute.bookingId,
guestId = currentRoute.guestId,
onBack = { refs.openActiveRoomStays(currentRoute.propertyId) },
onEditCheckout = { expectedCheckInAt, expectedCheckOutAt ->
refs.route.value = AppRoute.BookingExpectedDates(
propertyId = currentRoute.propertyId,
bookingId = currentRoute.bookingId,
status = "CHECKED_IN",
expectedCheckInAt = expectedCheckInAt,
expectedCheckOutAt = expectedCheckOutAt
)
},
onEditSignature = { guestId ->
refs.route.value = AppRoute.GuestSignature(
currentRoute.propertyId,
currentRoute.bookingId,
guestId
)
},
onOpenRazorpayQr = { pendingAmount, guestPhone ->
refs.route.value = AppRoute.RazorpayQr(
propertyId = currentRoute.propertyId,
bookingId = currentRoute.bookingId,
pendingAmount = pendingAmount,
guestPhone = guestPhone
)
},
onOpenPayments = {
refs.route.value = AppRoute.BookingPayments(
propertyId = currentRoute.propertyId,
bookingId = currentRoute.bookingId
)
},
canManageDocuments = authz.canManageGuestDocuments(currentRoute.propertyId)
)
is AppRoute.BookingPayments -> BookingPaymentsScreen(
propertyId = currentRoute.propertyId,
bookingId = currentRoute.bookingId,
canAddCash = authz.canAddBookingPayment(currentRoute.propertyId),
canDeleteCash = authz.canDeleteCashPayment(currentRoute.propertyId),
canRefund = authz.canRefundBookingPayment(currentRoute.propertyId),
onBack = {
refs.route.value = AppRoute.BookingDetailsTabs(
currentRoute.propertyId,
currentRoute.bookingId,
null
)
}
)
else -> return false
}
return true
}

View File

@@ -0,0 +1,117 @@
package com.android.trisolarispms.ui.navigation
import androidx.compose.runtime.Composable
import com.android.trisolarispms.core.auth.Role
import com.android.trisolarispms.ui.navigation.AppRoute
import com.android.trisolarispms.ui.auth.AuthUiState
import com.android.trisolarispms.ui.auth.AuthViewModel
import com.android.trisolarispms.ui.booking.BookingCreateScreen
import com.android.trisolarispms.ui.guest.GuestInfoScreen
import com.android.trisolarispms.ui.guest.GuestSignatureScreen
import com.android.trisolarispms.ui.home.HomeScreen
import com.android.trisolarispms.ui.property.AddPropertyScreen
@Composable
internal fun renderHomeGuestRoutes(
refs: MainUiRefs,
state: AuthUiState,
authViewModel: AuthViewModel
): Boolean {
when (val currentRoute = refs.currentRoute) {
AppRoute.Home -> HomeScreen(
userId = state.userId,
userName = state.userName,
isSuperAdmin = state.isSuperAdmin,
onUserDirectory = { refs.route.value = AppRoute.SuperAdminUsers },
onLogout = authViewModel::signOut,
onAddProperty = { refs.route.value = AppRoute.AddProperty },
onAmenities = {
refs.amenitiesReturnRoute.value = AppRoute.Home
refs.route.value = AppRoute.Amenities
},
onImageTags = { refs.route.value = AppRoute.ImageTags },
refreshKey = refs.refreshKey.value,
selectedPropertyId = refs.selectedPropertyId.value,
onSelectProperty = { id, name ->
refs.selectedPropertyId.value = id
refs.selectedPropertyName.value = name
refs.route.value = AppRoute.ActiveRoomStays(id, name)
},
onRefreshProfile = authViewModel::refreshMe,
showJoinProperty = state.propertyRoles.isEmpty() && !state.isSuperAdmin,
onJoinPropertySuccess = { joinedPropertyId, joinedRoles ->
refs.refreshKey.value++
authViewModel.refreshMe()
val isAdmin = joinedRoles.contains(Role.ADMIN)
val shouldAutoOpen = !state.isSuperAdmin && !isAdmin && state.propertyRoles.size <= 1
if (shouldAutoOpen) {
refs.openActiveRoomStays(joinedPropertyId)
}
}
)
AppRoute.AddProperty -> AddPropertyScreen(
onBack = { refs.route.value = AppRoute.Home },
onCreated = {
refs.refreshKey.value++
refs.route.value = AppRoute.Home
}
)
is AppRoute.CreateBooking -> BookingCreateScreen(
propertyId = currentRoute.propertyId,
onBack = { refs.openActiveRoomStays(currentRoute.propertyId) },
onCreated = { response, guest, phone ->
val bookingId = response.id.orEmpty()
val guestId = (guest?.id ?: response.guestId).orEmpty()
refs.selectedGuest.value = guest
refs.selectedGuestPhone.value = phone
if (bookingId.isNotBlank() && guestId.isNotBlank()) {
val fromAt = response.checkInAt?.takeIf { it.isNotBlank() }
?: response.expectedCheckInAt.orEmpty()
val toAt = response.expectedCheckOutAt?.takeIf { it.isNotBlank() }
refs.route.value = AppRoute.ManageRoomStaySelectFromBooking(
propertyId = currentRoute.propertyId,
bookingId = bookingId,
guestId = guestId,
fromAt = fromAt,
toAt = toAt
)
} else {
refs.route.value = AppRoute.Home
}
}
)
is AppRoute.GuestInfo -> GuestInfoScreen(
propertyId = currentRoute.propertyId,
guestId = currentRoute.guestId,
initialGuest = refs.selectedGuest.value,
initialPhone = refs.selectedGuestPhone.value,
onBack = { refs.route.value = AppRoute.Home },
onSave = {
refs.route.value = AppRoute.GuestSignature(
currentRoute.propertyId,
currentRoute.bookingId,
currentRoute.guestId
)
}
)
is AppRoute.GuestSignature -> GuestSignatureScreen(
propertyId = currentRoute.propertyId,
guestId = currentRoute.guestId,
onBack = {
refs.route.value = AppRoute.GuestInfo(
currentRoute.propertyId,
currentRoute.bookingId,
currentRoute.guestId
)
},
onDone = { refs.openActiveRoomStays(currentRoute.propertyId) }
)
else -> return false
}
return true
}

View File

@@ -0,0 +1,210 @@
package com.android.trisolarispms.ui.navigation
import androidx.compose.runtime.Composable
import com.android.trisolarispms.core.auth.AuthzPolicy
import com.android.trisolarispms.core.auth.toRoleNameList
import com.android.trisolarispms.data.api.model.AmenityDto
import com.android.trisolarispms.data.api.model.RoomImageTagDto
import com.android.trisolarispms.data.api.model.RoomTypeDto
import com.android.trisolarispms.ui.navigation.AppRoute
import com.android.trisolarispms.ui.card.CardInfoScreen
import com.android.trisolarispms.ui.card.IssueTemporaryCardScreen
import com.android.trisolarispms.ui.room.RoomFormScreen
import com.android.trisolarispms.ui.room.RoomsScreen
import com.android.trisolarispms.ui.roomimage.AddImageTagScreen
import com.android.trisolarispms.ui.roomimage.EditImageTagScreen
import com.android.trisolarispms.ui.roomimage.ImageTagsScreen
import com.android.trisolarispms.ui.roomimage.RoomImagesScreen
import com.android.trisolarispms.ui.roomtype.AddAmenityScreen
import com.android.trisolarispms.ui.roomtype.AddRoomTypeScreen
import com.android.trisolarispms.ui.roomtype.AmenitiesScreen
import com.android.trisolarispms.ui.roomtype.EditAmenityScreen
import com.android.trisolarispms.ui.roomtype.EditRoomTypeScreen
import com.android.trisolarispms.ui.roomtype.RatePlanCalendarScreen
import com.android.trisolarispms.ui.roomtype.RoomTypesScreen
import com.android.trisolarispms.ui.users.PropertyAccessCodeScreen
import com.android.trisolarispms.ui.users.PropertyUsersScreen
import com.android.trisolarispms.ui.users.SuperAdminUserDirectoryScreen
@Composable
internal fun renderManagementRoutes(
refs: MainUiRefs,
stateIsSuperAdmin: Boolean,
authz: AuthzPolicy
): Boolean {
when (val currentRoute = refs.currentRoute) {
AppRoute.SuperAdminUsers -> SuperAdminUserDirectoryScreen(
onBack = { refs.route.value = AppRoute.Home }
)
is AppRoute.PropertyUsers -> PropertyUsersScreen(
propertyId = currentRoute.propertyId,
allowedRoleAssignments = authz
.allowedRoleAssignments(currentRoute.propertyId)
.toRoleNameList(),
canDisableAdmin = authz.canDisableAdmin(currentRoute.propertyId),
canDisableManager = authz.canDisableManager(currentRoute.propertyId),
onBack = { refs.openActiveRoomStays(currentRoute.propertyId) },
onOpenAccessCode = {
refs.route.value = AppRoute.PropertyAccessCode(currentRoute.propertyId)
}
)
is AppRoute.PropertyAccessCode -> PropertyAccessCodeScreen(
propertyId = currentRoute.propertyId,
allowedRoles = authz.allowedAccessCodeRoles(currentRoute.propertyId).toRoleNameList(),
onBack = { refs.route.value = AppRoute.PropertyUsers(currentRoute.propertyId) }
)
is AppRoute.Rooms -> RoomsScreen(
propertyId = currentRoute.propertyId,
onBack = { refs.openActiveRoomStays(currentRoute.propertyId) },
onAddRoom = {
refs.roomFormKey.value++
refs.route.value = AppRoute.AddRoom(currentRoute.propertyId)
},
onViewRoomTypes = { refs.route.value = AppRoute.RoomTypes(currentRoute.propertyId) },
onViewCardInfo = { refs.route.value = AppRoute.CardInfo(currentRoute.propertyId) },
canManageRooms = authz.canManageProperty(currentRoute.propertyId),
canViewCardInfo = authz.canViewCardInfo(currentRoute.propertyId),
canIssueTemporaryCard = authz.canIssueTemporaryCard(currentRoute.propertyId),
onEditRoom = {
refs.selectedRoom.value = it
refs.roomFormKey.value++
refs.route.value = AppRoute.EditRoom(currentRoute.propertyId, it.id ?: "")
},
onIssueTemporaryCard = {
if (it.id != null && authz.canIssueTemporaryCard(currentRoute.propertyId)) {
refs.selectedRoom.value = it
refs.route.value = AppRoute.IssueTemporaryCard(currentRoute.propertyId, it.id)
}
}
)
is AppRoute.RoomTypes -> RoomTypesScreen(
propertyId = currentRoute.propertyId,
onBack = { refs.route.value = AppRoute.Rooms(currentRoute.propertyId) },
onAdd = { refs.route.value = AppRoute.AddRoomType(currentRoute.propertyId) },
canManageRoomTypes = authz.canManageProperty(currentRoute.propertyId),
onEdit = {
refs.selectedRoomType.value = it
refs.route.value = AppRoute.EditRoomType(currentRoute.propertyId, it.id ?: "")
}
)
is AppRoute.AddRoomType -> AddRoomTypeScreen(
propertyId = currentRoute.propertyId,
onBack = { refs.route.value = AppRoute.RoomTypes(currentRoute.propertyId) },
onSave = { refs.route.value = AppRoute.RoomTypes(currentRoute.propertyId) }
)
is AppRoute.EditRoomType -> EditRoomTypeScreen(
propertyId = currentRoute.propertyId,
roomType = refs.selectedRoomType.value
?: RoomTypeDto(id = currentRoute.roomTypeId, code = "", name = ""),
onBack = { refs.route.value = AppRoute.RoomTypes(currentRoute.propertyId) },
onSave = { refs.route.value = AppRoute.RoomTypes(currentRoute.propertyId) },
onOpenRatePlanCalendar = { ratePlanId, ratePlanCode ->
refs.route.value = AppRoute.RatePlanCalendar(
currentRoute.propertyId,
currentRoute.roomTypeId,
ratePlanId,
ratePlanCode
)
}
)
AppRoute.Amenities -> AmenitiesScreen(
onBack = { refs.route.value = refs.amenitiesReturnRoute.value },
onAdd = { refs.route.value = AppRoute.AddAmenity },
canManageAmenities = stateIsSuperAdmin,
onEdit = {
refs.selectedAmenity.value = it
refs.route.value = AppRoute.EditAmenity(it.id ?: "")
}
)
AppRoute.AddAmenity -> AddAmenityScreen(
onBack = { refs.route.value = AppRoute.Amenities },
onSave = { refs.route.value = AppRoute.Amenities }
)
is AppRoute.EditAmenity -> EditAmenityScreen(
amenity = refs.selectedAmenity.value ?: AmenityDto(id = currentRoute.amenityId, name = ""),
onBack = { refs.route.value = AppRoute.Amenities },
onSave = { refs.route.value = AppRoute.Amenities }
)
AppRoute.ImageTags -> ImageTagsScreen(
onBack = { refs.route.value = AppRoute.Home },
onAdd = { refs.route.value = AppRoute.AddImageTag },
onEdit = {
refs.selectedImageTag.value = it
refs.route.value = AppRoute.EditImageTag(it.id ?: "")
}
)
AppRoute.AddImageTag -> AddImageTagScreen(
onBack = { refs.route.value = AppRoute.ImageTags },
onSave = { refs.route.value = AppRoute.ImageTags }
)
is AppRoute.EditImageTag -> EditImageTagScreen(
tag = refs.selectedImageTag.value ?: RoomImageTagDto(id = currentRoute.tagId, name = ""),
onBack = { refs.route.value = AppRoute.ImageTags },
onSave = { refs.route.value = AppRoute.ImageTags }
)
is AppRoute.AddRoom -> RoomFormScreen(
title = "Add Room",
propertyId = currentRoute.propertyId,
roomId = null,
roomData = null,
formKey = refs.roomFormKey.value,
onBack = { refs.route.value = AppRoute.Rooms(currentRoute.propertyId) },
onSave = { refs.route.value = AppRoute.Rooms(currentRoute.propertyId) },
onViewImages = { }
)
is AppRoute.EditRoom -> RoomFormScreen(
title = "Modify Room",
propertyId = currentRoute.propertyId,
roomId = currentRoute.roomId,
roomData = refs.selectedRoom.value,
formKey = refs.roomFormKey.value,
onBack = { refs.route.value = AppRoute.Rooms(currentRoute.propertyId) },
onSave = { refs.route.value = AppRoute.Rooms(currentRoute.propertyId) },
onViewImages = { roomId ->
refs.route.value = AppRoute.RoomImages(currentRoute.propertyId, roomId)
}
)
is AppRoute.IssueTemporaryCard -> IssueTemporaryCardScreen(
propertyId = currentRoute.propertyId,
roomId = currentRoute.roomId,
roomNumber = refs.selectedRoom.value?.roomNumber?.toString(),
onBack = { refs.route.value = AppRoute.Rooms(currentRoute.propertyId) }
)
is AppRoute.CardInfo -> CardInfoScreen(
propertyId = currentRoute.propertyId,
onBack = { refs.route.value = AppRoute.Rooms(currentRoute.propertyId) }
)
is AppRoute.RatePlanCalendar -> RatePlanCalendarScreen(
propertyId = currentRoute.propertyId,
ratePlanId = currentRoute.ratePlanId,
ratePlanCode = currentRoute.ratePlanCode,
onBack = { refs.route.value = AppRoute.EditRoomType(currentRoute.propertyId, currentRoute.roomTypeId) }
)
is AppRoute.RoomImages -> RoomImagesScreen(
propertyId = currentRoute.propertyId,
roomId = currentRoute.roomId,
onBack = { refs.route.value = AppRoute.EditRoom(currentRoute.propertyId, currentRoute.roomId) }
)
else -> return false
}
return true
}

View File

@@ -0,0 +1,171 @@
package com.android.trisolarispms.ui.navigation
import androidx.compose.runtime.Composable
import com.android.trisolarispms.core.auth.AuthzPolicy
import com.android.trisolarispms.ui.navigation.AppRoute
import com.android.trisolarispms.ui.auth.AuthUiState
import com.android.trisolarispms.ui.auth.AuthViewModel
import com.android.trisolarispms.ui.razorpay.RazorpayQrScreen
import com.android.trisolarispms.ui.razorpay.RazorpaySettingsScreen
import com.android.trisolarispms.ui.roomstay.ActiveRoomStaysScreen
import com.android.trisolarispms.ui.roomstay.ManageRoomStayRatesScreen
import com.android.trisolarispms.ui.roomstay.ManageRoomStaySelectScreen
@Composable
internal fun renderStayFlowRoutes(
refs: MainUiRefs,
state: AuthUiState,
authViewModel: AuthViewModel,
authz: AuthzPolicy
): Boolean {
when (val currentRoute = refs.currentRoute) {
is AppRoute.ActiveRoomStays -> ActiveRoomStaysScreen(
propertyId = currentRoute.propertyId,
propertyName = currentRoute.propertyName,
onBack = {
val blockBack = authz.shouldBlockBackToHome(
propertyId = currentRoute.propertyId,
propertyCount = state.propertyRoles.size
)
if (!blockBack) {
refs.route.value = AppRoute.Home
}
},
showBack = !authz.shouldBlockBackToHome(
propertyId = currentRoute.propertyId,
propertyCount = state.propertyRoles.size
),
onViewRooms = { refs.route.value = AppRoute.Rooms(currentRoute.propertyId) },
onCreateBooking = { refs.route.value = AppRoute.CreateBooking(currentRoute.propertyId) },
canCreateBooking = authz.canCreateBookingFor(currentRoute.propertyId),
showRazorpaySettings = authz.canManageRazorpaySettings(currentRoute.propertyId),
onRazorpaySettings = { refs.route.value = AppRoute.RazorpaySettings(currentRoute.propertyId) },
showUserAdmin = authz.canManagePropertyUsers(currentRoute.propertyId),
onUserAdmin = { refs.route.value = AppRoute.PropertyUsers(currentRoute.propertyId) },
onLogout = authViewModel::signOut,
onManageRoomStay = { booking ->
val fromAt = booking.checkInAt?.takeIf { it.isNotBlank() }
?: booking.expectedCheckInAt.orEmpty()
val toAt = booking.expectedCheckOutAt?.takeIf { it.isNotBlank() }
?: booking.checkOutAt?.takeIf { it.isNotBlank() }
if (fromAt.isNotBlank()) {
refs.route.value = AppRoute.ManageRoomStaySelect(
propertyId = currentRoute.propertyId,
bookingId = booking.id.orEmpty(),
fromAt = fromAt,
toAt = toAt
)
}
},
onViewBookingStays = { booking ->
refs.route.value = AppRoute.BookingRoomStays(
propertyId = currentRoute.propertyId,
bookingId = booking.id.orEmpty()
)
},
onOpenBookingDetails = { booking ->
refs.route.value = AppRoute.BookingDetailsTabs(
propertyId = currentRoute.propertyId,
bookingId = booking.id.orEmpty(),
guestId = booking.guestId
)
}
)
is AppRoute.RazorpaySettings -> RazorpaySettingsScreen(
propertyId = currentRoute.propertyId,
onBack = { refs.openActiveRoomStays(currentRoute.propertyId) }
)
is AppRoute.RazorpayQr -> RazorpayQrScreen(
propertyId = currentRoute.propertyId,
bookingId = currentRoute.bookingId,
pendingAmount = currentRoute.pendingAmount,
guestPhone = currentRoute.guestPhone,
onBack = {
refs.route.value = AppRoute.BookingDetailsTabs(
currentRoute.propertyId,
currentRoute.bookingId,
null
)
}
)
is AppRoute.ManageRoomStaySelect -> ManageRoomStaySelectScreen(
propertyId = currentRoute.propertyId,
bookingFromAt = currentRoute.fromAt,
bookingToAt = currentRoute.toAt,
onBack = { refs.openActiveRoomStays(currentRoute.propertyId) },
onNext = { rooms ->
refs.selectedManageRooms.value = rooms
refs.route.value = AppRoute.ManageRoomStayRates(
propertyId = currentRoute.propertyId,
bookingId = currentRoute.bookingId,
fromAt = currentRoute.fromAt,
toAt = currentRoute.toAt
)
}
)
is AppRoute.ManageRoomStaySelectFromBooking -> ManageRoomStaySelectScreen(
propertyId = currentRoute.propertyId,
bookingFromAt = currentRoute.fromAt,
bookingToAt = currentRoute.toAt,
onBack = { refs.openActiveRoomStays(currentRoute.propertyId) },
onNext = { rooms ->
refs.selectedManageRooms.value = rooms
refs.route.value = AppRoute.ManageRoomStayRatesFromBooking(
propertyId = currentRoute.propertyId,
bookingId = currentRoute.bookingId,
guestId = currentRoute.guestId,
fromAt = currentRoute.fromAt,
toAt = currentRoute.toAt
)
}
)
is AppRoute.ManageRoomStayRates -> ManageRoomStayRatesScreen(
propertyId = currentRoute.propertyId,
bookingId = currentRoute.bookingId,
checkInAt = currentRoute.fromAt,
checkOutAt = currentRoute.toAt,
selectedRooms = refs.selectedManageRooms.value,
onBack = {
refs.route.value = AppRoute.ManageRoomStaySelect(
currentRoute.propertyId,
currentRoute.bookingId,
currentRoute.fromAt,
currentRoute.toAt
)
},
onDone = { refs.openActiveRoomStays(currentRoute.propertyId) }
)
is AppRoute.ManageRoomStayRatesFromBooking -> ManageRoomStayRatesScreen(
propertyId = currentRoute.propertyId,
bookingId = currentRoute.bookingId,
checkInAt = currentRoute.fromAt,
checkOutAt = currentRoute.toAt,
selectedRooms = refs.selectedManageRooms.value,
onBack = {
refs.route.value = AppRoute.ManageRoomStaySelectFromBooking(
currentRoute.propertyId,
currentRoute.bookingId,
currentRoute.guestId,
currentRoute.fromAt,
currentRoute.toAt
)
},
onDone = {
refs.route.value = AppRoute.GuestInfo(
currentRoute.propertyId,
currentRoute.bookingId,
currentRoute.guestId
)
}
)
else -> return false
}
return true
}

View File

@@ -0,0 +1,33 @@
package com.android.trisolarispms.ui.navigation
import androidx.compose.runtime.MutableState
import com.android.trisolarispms.data.api.model.AmenityDto
import com.android.trisolarispms.data.api.model.GuestDto
import com.android.trisolarispms.data.api.model.RoomDto
import com.android.trisolarispms.data.api.model.RoomImageTagDto
import com.android.trisolarispms.data.api.model.RoomTypeDto
import com.android.trisolarispms.ui.navigation.AppRoute
import com.android.trisolarispms.ui.roomstay.ManageRoomStaySelection
internal data class MainUiRefs(
val route: MutableState<AppRoute>,
val refreshKey: MutableState<Int>,
val selectedPropertyId: MutableState<String?>,
val selectedPropertyName: MutableState<String?>,
val selectedRoom: MutableState<RoomDto?>,
val selectedRoomType: MutableState<RoomTypeDto?>,
val selectedAmenity: MutableState<AmenityDto?>,
val selectedGuest: MutableState<GuestDto?>,
val selectedGuestPhone: MutableState<String?>,
val selectedImageTag: MutableState<RoomImageTagDto?>,
val selectedManageRooms: MutableState<List<ManageRoomStaySelection>>,
val roomFormKey: MutableState<Int>,
val amenitiesReturnRoute: MutableState<AppRoute>
) {
val currentRoute: AppRoute
get() = route.value
fun openActiveRoomStays(propertyId: String) {
route.value = AppRoute.ActiveRoomStays(propertyId, selectedPropertyName.value ?: "Property")
}
}

View File

@@ -240,7 +240,12 @@ private fun PaymentCard(
payment.currency?.let { append(" $it") }
}
Text(text = amountText, style = MaterialTheme.typography.titleMedium)
if (canRefund && (!payment.id.isNullOrBlank() || !payment.gatewayPaymentId.isNullOrBlank())) {
val hasRefundableAmount = (payment.amount ?: 0L) > 0L
if (
canRefund &&
hasRefundableAmount &&
(!payment.id.isNullOrBlank() || !payment.gatewayPaymentId.isNullOrBlank())
) {
TextButton(onClick = { onRefund(payment) }) {
Text("Refund")
}

View File

@@ -2,7 +2,7 @@ package com.android.trisolarispms.ui.payment
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.ApiClient
import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.data.api.model.RazorpayRefundRequest
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow

View File

@@ -2,7 +2,7 @@ package com.android.trisolarispms.ui.property
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.ApiClient
import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.data.api.model.PropertyCreateRequest
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow

View File

@@ -2,7 +2,7 @@ package com.android.trisolarispms.ui.property
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.ApiClient
import com.android.trisolarispms.data.api.core.ApiClient
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update

View File

@@ -2,8 +2,8 @@ package com.android.trisolarispms.ui.razorpay
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.ApiClient
import com.android.trisolarispms.data.api.ApiConstants
import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.data.api.core.ApiConstants
import com.android.trisolarispms.data.api.model.RazorpayCloseRequest
import com.android.trisolarispms.data.api.model.RazorpayQrEventDto
import com.android.trisolarispms.data.api.model.RazorpayRequestListItemDto

View File

@@ -2,7 +2,7 @@ package com.android.trisolarispms.ui.razorpay
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.ApiClient
import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.data.api.model.RazorpaySettingsRequest
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow

View File

@@ -2,7 +2,7 @@ package com.android.trisolarispms.ui.room
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.ApiClient
import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.data.api.model.RoomCreateRequest
import com.android.trisolarispms.data.api.model.RoomUpdateRequest
import kotlinx.coroutines.flow.MutableStateFlow

View File

@@ -2,7 +2,7 @@ package com.android.trisolarispms.ui.room
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.ApiClient
import com.android.trisolarispms.data.api.core.ApiClient
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update

View File

@@ -2,7 +2,7 @@ package com.android.trisolarispms.ui.roomimage
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.ApiClient
import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.data.api.model.RoomImageTagDto
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow

View File

@@ -2,7 +2,7 @@ package com.android.trisolarispms.ui.roomimage
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.ApiClient
import com.android.trisolarispms.data.api.core.ApiClient
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update

View File

@@ -2,7 +2,7 @@ package com.android.trisolarispms.ui.roomimage
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.ApiClient
import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.data.api.model.ImageDto
import com.android.trisolarispms.data.api.model.RoomImageReorderRequest
import kotlinx.coroutines.flow.MutableStateFlow

View File

@@ -2,7 +2,7 @@ package com.android.trisolarispms.ui.roomstay
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.ApiClient
import com.android.trisolarispms.data.api.core.ApiClient
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update

View File

@@ -53,8 +53,8 @@ import coil.ImageLoader
import coil.compose.AsyncImage
import coil.decode.SvgDecoder
import coil.request.ImageRequest
import com.android.trisolarispms.data.api.ApiConstants
import com.android.trisolarispms.data.api.FirebaseAuthTokenProvider
import com.android.trisolarispms.data.api.core.ApiConstants
import com.android.trisolarispms.data.api.core.FirebaseAuthTokenProvider
import com.android.trisolarispms.data.api.model.BookingDetailsResponse
import com.android.trisolarispms.ui.guestdocs.GuestDocumentsTab
import com.google.firebase.auth.FirebaseAuth
@@ -87,6 +87,10 @@ fun BookingDetailsTabsScreen(
val scope = rememberCoroutineScope()
val staysState by staysViewModel.state.collectAsState()
val detailsState by detailsViewModel.state.collectAsState()
val canModifyDocuments = canManageDocuments && when (detailsState.details?.status) {
"OPEN", "CHECKED_IN" -> true
else -> false
}
LaunchedEffect(propertyId, bookingId, guestId) {
staysViewModel.load(propertyId, bookingId)
@@ -164,7 +168,8 @@ fun BookingDetailsTabsScreen(
propertyId = propertyId,
guestId = resolvedGuestId,
bookingId = bookingId,
canManageDocuments = canManageDocuments
canManageDocuments = canManageDocuments,
canModifyDocuments = canModifyDocuments
)
} else {
Box(

View File

@@ -2,8 +2,8 @@ package com.android.trisolarispms.ui.roomstay
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.ApiClient
import com.android.trisolarispms.data.api.ApiConstants
import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.data.api.core.ApiConstants
import com.android.trisolarispms.data.api.model.BookingDetailsResponse
import com.google.gson.Gson
import kotlinx.coroutines.flow.MutableStateFlow

View File

@@ -2,7 +2,7 @@ package com.android.trisolarispms.ui.roomstay
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.ApiClient
import com.android.trisolarispms.data.api.core.ApiClient
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update

View File

@@ -2,7 +2,7 @@ package com.android.trisolarispms.ui.roomstay
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.ApiClient
import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.data.api.model.BookingBulkCheckInRequest
import com.android.trisolarispms.data.api.model.BookingBulkCheckInStayRequest
import kotlinx.coroutines.flow.MutableStateFlow

View File

@@ -2,7 +2,7 @@ package com.android.trisolarispms.ui.roomstay
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.ApiClient
import com.android.trisolarispms.data.api.core.ApiClient
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update

View File

@@ -33,7 +33,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.android.trisolarispms.data.api.model.AmenityDto
import com.android.trisolarispms.data.api.ApiConstants
import com.android.trisolarispms.data.api.core.ApiConstants
import coil.compose.AsyncImage
@Composable

View File

@@ -43,7 +43,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.PopupProperties
import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.AsyncImage
import com.android.trisolarispms.data.api.ApiConstants
import com.android.trisolarispms.data.api.core.ApiConstants
@Composable
@OptIn(ExperimentalMaterial3Api::class)

View File

@@ -2,7 +2,7 @@ package com.android.trisolarispms.ui.roomtype
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.ApiClient
import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.data.api.model.AmenityCreateRequest
import com.android.trisolarispms.data.api.model.AmenityUpdateRequest
import kotlinx.coroutines.flow.MutableStateFlow

View File

@@ -2,7 +2,7 @@ package com.android.trisolarispms.ui.roomtype
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.ApiClient
import com.android.trisolarispms.data.api.core.ApiClient
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update

View File

@@ -2,7 +2,7 @@ package com.android.trisolarispms.ui.roomtype
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.ApiClient
import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.data.api.model.RatePlanCalendarEntry
import com.android.trisolarispms.data.api.model.RatePlanCalendarUpsertRequest
import com.android.trisolarispms.data.api.model.RatePlanRequest

View File

@@ -44,7 +44,7 @@ import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.AsyncImage
import com.android.trisolarispms.data.api.ApiConstants
import com.android.trisolarispms.data.api.core.ApiConstants
@Composable
@OptIn(ExperimentalMaterial3Api::class)

View File

@@ -2,7 +2,7 @@ package com.android.trisolarispms.ui.roomtype
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.ApiClient
import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.data.api.model.RoomTypeCreateRequest
import com.android.trisolarispms.data.api.model.RoomTypeUpdateRequest
import kotlinx.coroutines.flow.MutableStateFlow

View File

@@ -2,7 +2,7 @@ package com.android.trisolarispms.ui.roomtype
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.ApiClient
import com.android.trisolarispms.data.api.core.ApiClient
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update

View File

@@ -2,7 +2,7 @@ package com.android.trisolarispms.ui.users
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.ApiClient
import com.android.trisolarispms.data.api.core.ApiClient
import com.android.trisolarispms.data.api.model.PropertyAccessCodeCreateRequest
import com.android.trisolarispms.data.api.model.PropertyAccessCodeResponse
import com.android.trisolarispms.data.api.model.PropertyUserResponse

View File

@@ -14,6 +14,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.android.trisolarispms.core.auth.Role
import com.android.trisolarispms.core.auth.toRoles
@Composable
fun PropertyUserCard(
@@ -73,6 +75,14 @@ fun canDisableUser(
if (user.userId.isNullOrBlank()) return false
if (canDisableAdmin) return true
if (!canDisableManager) return false
val allowed = setOf("STAFF", "AGENT", "HOUSEKEEPING", "FINANCE", "GUIDE", "SUPERVISOR")
return user.roles.all { allowed.contains(it) }
val allowed = setOf(
Role.STAFF,
Role.AGENT,
Role.HOUSEKEEPING,
Role.FINANCE,
Role.GUIDE,
Role.SUPERVISOR
)
val parsedRoles = user.roles.toRoles()
return parsedRoles.size == user.roles.size && parsedRoles.all { it in allowed }
}

View File

@@ -2,7 +2,7 @@ package com.android.trisolarispms.ui.users
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.trisolarispms.data.api.ApiClient
import com.android.trisolarispms.data.api.core.ApiClient
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
@@ -32,18 +32,9 @@ class UserDirectoryViewModel : ViewModel() {
when (mode) {
UserDirectoryMode.SuperAdmin -> {
val response = api.listUsers(null)
val body = response.body()
if (response.isSuccessful && body != null) {
val mapped = body.map {
PropertyUserUi(
userId = it.id,
name = it.name,
phoneE164 = it.phoneE164,
disabled = it.disabled,
superAdmin = it.superAdmin
)
}
_state.update { it.copy(isLoading = false, users = mapped, error = null) }
val users = response.body()?.toSuperAdminUsers()
if (response.isSuccessful && users != null) {
_state.update { it.copy(isLoading = false, users = users, error = null) }
} else {
_state.update { it.copy(isLoading = false, error = "Load failed: ${response.code()}") }
}
@@ -53,19 +44,9 @@ class UserDirectoryViewModel : ViewModel() {
propertyId = mode.propertyId,
phone = null
)
val body = response.body()
if (response.isSuccessful && body != null) {
val mapped = body.map {
PropertyUserUi(
userId = it.userId,
roles = it.roles,
name = it.name,
phoneE164 = it.phoneE164,
disabled = it.disabled,
superAdmin = it.superAdmin
)
}
_state.update { it.copy(isLoading = false, users = mapped, error = null) }
val users = response.body()?.toPropertyUsers()
if (response.isSuccessful && users != null) {
_state.update { it.copy(isLoading = false, users = users, error = null) }
} else {
_state.update { it.copy(isLoading = false, error = "Load failed: ${response.code()}") }
}
@@ -90,18 +71,9 @@ class UserDirectoryViewModel : ViewModel() {
when (mode) {
UserDirectoryMode.SuperAdmin -> {
val response = api.listUsers(digits)
val body = response.body()
if (response.isSuccessful && body != null) {
val mapped = body.map {
PropertyUserUi(
userId = it.id,
name = it.name,
phoneE164 = it.phoneE164,
disabled = it.disabled,
superAdmin = it.superAdmin
)
}
_state.update { it.copy(isLoading = false, users = mapped, error = null) }
val users = response.body()?.toSuperAdminUsers()
if (response.isSuccessful && users != null) {
_state.update { it.copy(isLoading = false, users = users, error = null) }
} else {
_state.update { it.copy(isLoading = false, error = "Search failed: ${response.code()}") }
}
@@ -111,19 +83,9 @@ class UserDirectoryViewModel : ViewModel() {
propertyId = mode.propertyId,
phone = digits
)
val body = response.body()
if (response.isSuccessful && body != null) {
val mapped = body.map {
PropertyUserUi(
userId = it.userId,
roles = it.roles,
name = it.name,
phoneE164 = it.phoneE164,
disabled = it.disabled,
superAdmin = it.superAdmin
)
}
_state.update { it.copy(isLoading = false, users = mapped, error = null) }
val users = response.body()?.toPropertyUsers()
if (response.isSuccessful && users != null) {
_state.update { it.copy(isLoading = false, users = users, error = null) }
} else {
_state.update { it.copy(isLoading = false, error = "Search failed: ${response.code()}") }
}
@@ -134,4 +96,28 @@ class UserDirectoryViewModel : ViewModel() {
}
}
}
private fun List<com.android.trisolarispms.data.api.model.AppUserSummaryResponse>.toSuperAdminUsers():
List<PropertyUserUi> =
map {
PropertyUserUi(
userId = it.id,
name = it.name,
phoneE164 = it.phoneE164,
disabled = it.disabled,
superAdmin = it.superAdmin
)
}
private fun List<com.android.trisolarispms.data.api.model.PropertyUserDetailsResponse>.toPropertyUsers():
List<PropertyUserUi> = map {
PropertyUserUi(
userId = it.userId,
roles = it.roles,
name = it.name,
phoneE164 = it.phoneE164,
disabled = it.disabled,
superAdmin = it.superAdmin
)
}
}