Add card info screen and revoke flow
This commit is contained in:
@@ -19,6 +19,7 @@ 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
|
||||
@@ -110,6 +111,7 @@ class MainActivity : ComponentActivity() {
|
||||
route.value = AppRoute.AddRoom(currentRoute.propertyId)
|
||||
},
|
||||
onViewRoomTypes = { route.value = AppRoute.RoomTypes(currentRoute.propertyId) },
|
||||
onViewCardInfo = { route.value = AppRoute.CardInfo(currentRoute.propertyId) },
|
||||
canManageRooms = canManageProperty(currentRoute.propertyId),
|
||||
onEditRoom = {
|
||||
selectedRoom.value = it
|
||||
@@ -210,6 +212,10 @@ class MainActivity : ComponentActivity() {
|
||||
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.RoomImages -> RoomImagesScreen(
|
||||
propertyId = currentRoute.propertyId,
|
||||
roomId = currentRoute.roomId,
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package com.android.trisolarispms.data.api
|
||||
|
||||
import com.android.trisolarispms.data.api.model.ActionResponse
|
||||
import com.android.trisolarispms.data.api.model.CardPrepareRequest
|
||||
import com.android.trisolarispms.data.api.model.CardPrepareResponse
|
||||
import com.android.trisolarispms.data.api.model.IssueCardRequest
|
||||
import com.android.trisolarispms.data.api.model.IssuedCardResponse
|
||||
import com.android.trisolarispms.data.api.model.RevokeCardResponse
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
@@ -50,5 +50,5 @@ interface CardApi {
|
||||
suspend fun revokeCard(
|
||||
@Path("propertyId") propertyId: String,
|
||||
@Path("cardId") cardId: String
|
||||
): Response<ActionResponse>
|
||||
): Response<RevokeCardResponse>
|
||||
}
|
||||
|
||||
@@ -34,3 +34,7 @@ data class IssuedCardResponse(
|
||||
val issuedByUserId: String? = null,
|
||||
val revokedAt: String? = null
|
||||
)
|
||||
|
||||
data class RevokeCardResponse(
|
||||
val timeData: String? = null
|
||||
)
|
||||
|
||||
@@ -30,7 +30,9 @@ data class RoomDto(
|
||||
val hasNfc: Boolean? = null,
|
||||
val active: Boolean? = null,
|
||||
val maintenance: Boolean? = null,
|
||||
val notes: String? = null
|
||||
val notes: String? = null,
|
||||
val tempCardActive: Boolean? = null,
|
||||
val tempCardExpiresAt: String? = null
|
||||
)
|
||||
|
||||
data class RoomBoardDto(
|
||||
|
||||
@@ -8,6 +8,7 @@ sealed interface AppRoute {
|
||||
data class AddRoom(val propertyId: String) : AppRoute
|
||||
data class EditRoom(val propertyId: String, val roomId: String) : AppRoute
|
||||
data class IssueTemporaryCard(val propertyId: String, val roomId: String) : AppRoute
|
||||
data class CardInfo(val propertyId: String) : AppRoute
|
||||
data class RoomTypes(val propertyId: String) : AppRoute
|
||||
data class AddRoomType(val propertyId: String) : AppRoute
|
||||
data class EditRoomType(val propertyId: String, val roomTypeId: String) : AppRoute
|
||||
|
||||
@@ -0,0 +1,382 @@
|
||||
package com.android.trisolarispms.ui.card
|
||||
|
||||
import android.app.Activity
|
||||
import android.nfc.NfcAdapter
|
||||
import android.nfc.Tag
|
||||
import android.nfc.tech.MifareClassic
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.airbnb.lottie.compose.LottieAnimation
|
||||
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 java.util.Calendar
|
||||
import java.util.Date
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
fun CardInfoScreen(
|
||||
propertyId: String,
|
||||
onBack: () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val activity = context as? Activity
|
||||
val nfcAdapter = NfcAdapter.getDefaultAdapter(context)
|
||||
val scope = rememberCoroutineScope()
|
||||
val lastTag = remember { mutableStateOf<Tag?>(null) }
|
||||
val cardId = remember { mutableStateOf<String?>(null) }
|
||||
val roomNumber = remember { mutableStateOf<String?>(null) }
|
||||
val cardIndex = remember { mutableStateOf<String?>(null) }
|
||||
val issuedBy = remember { mutableStateOf<String?>(null) }
|
||||
val issuedById = remember { mutableStateOf<String?>(null) }
|
||||
val issuedAt = remember { mutableStateOf<String?>(null) }
|
||||
val expiresAt = remember { mutableStateOf<String?>(null) }
|
||||
val expired = remember { mutableStateOf<Boolean?>(null) }
|
||||
val error = remember { mutableStateOf<String?>(null) }
|
||||
val revokeError = remember { mutableStateOf<String?>(null) }
|
||||
val revokeStatus = remember { mutableStateOf<String?>(null) }
|
||||
val showRevokeConfirm = remember { mutableStateOf(false) }
|
||||
val showRevokeSuccess = remember { mutableStateOf(false) }
|
||||
|
||||
val composition = rememberLottieComposition(
|
||||
LottieCompositionSpec.Asset("scan_nfc_animation.json")
|
||||
)
|
||||
val progress = animateLottieCompositionAsState(
|
||||
composition = composition.value,
|
||||
iterations = LottieConstants.IterateForever
|
||||
)
|
||||
|
||||
DisposableEffect(activity, nfcAdapter) {
|
||||
if (activity != null && nfcAdapter != null) {
|
||||
val flags = NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK
|
||||
val callback = NfcAdapter.ReaderCallback { tag ->
|
||||
lastTag.value = tag
|
||||
handleTag(
|
||||
tag = tag,
|
||||
cardId = cardId,
|
||||
roomNumber = roomNumber,
|
||||
cardIndex = cardIndex,
|
||||
issuedBy = issuedBy,
|
||||
issuedById = issuedById,
|
||||
issuedAt = issuedAt,
|
||||
expiresAt = expiresAt,
|
||||
expired = expired,
|
||||
error = error
|
||||
)
|
||||
}
|
||||
nfcAdapter.enableReaderMode(activity, callback, flags, null)
|
||||
onDispose { nfcAdapter.disableReaderMode(activity) }
|
||||
} else {
|
||||
onDispose { }
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Card Details") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = "Back")
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors()
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.padding(24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Top
|
||||
) {
|
||||
Text(
|
||||
text = "Tap a card on the back of the phone",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
LottieAnimation(
|
||||
composition = composition.value,
|
||||
progress = { progress.value },
|
||||
modifier = Modifier.size(180.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
error.value?.let {
|
||||
Text(text = it, color = MaterialTheme.colorScheme.error)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
revokeError.value?.let {
|
||||
Text(text = it, color = MaterialTheme.colorScheme.error)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
revokeStatus.value?.let {
|
||||
Text(text = it, color = MaterialTheme.colorScheme.primary)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
infoRow("Card ID", cardId.value)
|
||||
infoRow("Room", roomNumber.value)
|
||||
infoRow("Card Index", cardIndex.value)
|
||||
infoRow("Issued By", issuedBy.value)
|
||||
infoRow("Issuer ID", issuedById.value)
|
||||
infoRow("Issued At", issuedAt.value)
|
||||
infoRow("Expires At", expiresAt.value)
|
||||
expired.value?.let {
|
||||
infoRow("Expired", if (it) "Yes" else "No")
|
||||
}
|
||||
if (!cardId.value.isNullOrBlank() && expired.value != true) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
TextButton(onClick = { showRevokeConfirm.value = true }) {
|
||||
Text("Revoke Card")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showRevokeConfirm.value) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showRevokeConfirm.value = false },
|
||||
title = { Text("Revoke card?") },
|
||||
text = { Text("This will revoke the current card and make it inactive.") },
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
showRevokeConfirm.value = false
|
||||
val currentIndex = cardIndex.value
|
||||
val currentTag = lastTag.value
|
||||
if (currentIndex.isNullOrBlank() || currentTag == null) {
|
||||
revokeError.value = "Place the card on the phone to revoke."
|
||||
return@TextButton
|
||||
}
|
||||
val indexValue = currentIndex.toIntOrNull()
|
||||
if (indexValue == null) {
|
||||
revokeError.value = "Invalid card index."
|
||||
return@TextButton
|
||||
}
|
||||
revokeError.value = null
|
||||
revokeStatus.value = "Revoking..."
|
||||
scope.launch {
|
||||
try {
|
||||
val api = ApiClient.create()
|
||||
val response = api.revokeCard(propertyId, indexValue.toString())
|
||||
val body = response.body()
|
||||
if (response.isSuccessful && body?.timeData != null) {
|
||||
val writeResult = writeRevokeTimeData(currentTag, body.timeData)
|
||||
if (writeResult == null) {
|
||||
revokeStatus.value = "Card revoked."
|
||||
showRevokeSuccess.value = true
|
||||
expired.value = true
|
||||
cardId.value = null
|
||||
roomNumber.value = null
|
||||
cardIndex.value = null
|
||||
issuedBy.value = null
|
||||
issuedById.value = null
|
||||
issuedAt.value = null
|
||||
expiresAt.value = null
|
||||
error.value = null
|
||||
} else {
|
||||
revokeStatus.value = null
|
||||
revokeError.value = writeResult
|
||||
}
|
||||
} else {
|
||||
revokeStatus.value = null
|
||||
revokeError.value = "Revoke failed: ${response.code()}"
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
revokeStatus.value = null
|
||||
revokeError.value = e.localizedMessage ?: "Revoke failed."
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text("Revoke")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showRevokeConfirm.value = false }) {
|
||||
Text("Cancel")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (showRevokeSuccess.value) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showRevokeSuccess.value = false },
|
||||
title = { Text("Card revoked") },
|
||||
text = { Text("Card revoked successfully.") },
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
showRevokeSuccess.value = false
|
||||
onBack()
|
||||
}
|
||||
) {
|
||||
Text("OK")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun infoRow(label: String, value: String?) {
|
||||
if (value.isNullOrBlank()) return
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
Text(
|
||||
text = "$label: $value",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleTag(
|
||||
tag: Tag,
|
||||
cardId: androidx.compose.runtime.MutableState<String?>,
|
||||
roomNumber: androidx.compose.runtime.MutableState<String?>,
|
||||
cardIndex: androidx.compose.runtime.MutableState<String?>,
|
||||
issuedBy: androidx.compose.runtime.MutableState<String?>,
|
||||
issuedById: androidx.compose.runtime.MutableState<String?>,
|
||||
issuedAt: androidx.compose.runtime.MutableState<String?>,
|
||||
expiresAt: androidx.compose.runtime.MutableState<String?>,
|
||||
expired: androidx.compose.runtime.MutableState<Boolean?>,
|
||||
error: androidx.compose.runtime.MutableState<String?>
|
||||
) {
|
||||
val mifare = MifareClassic.get(tag) ?: run {
|
||||
error.value = "Unsupported card type (not Mifare Classic)."
|
||||
return
|
||||
}
|
||||
try {
|
||||
mifare.connect()
|
||||
val sectorIndex = 0
|
||||
val authA = mifare.authenticateSectorWithKeyA(sectorIndex, MifareClassic.KEY_DEFAULT)
|
||||
val authB = mifare.authenticateSectorWithKeyB(sectorIndex, MifareClassic.KEY_DEFAULT)
|
||||
if (!authA && !authB) {
|
||||
error.value = "Authentication failed for sector 0."
|
||||
return
|
||||
}
|
||||
val key = getBlockHex(1, mifare)
|
||||
val timeData = getBlockHex(2, mifare)
|
||||
cardId.value = tag.id.joinToString("") { String.format("%02X", it) }
|
||||
if (key.length >= 32) {
|
||||
roomNumber.value = key.substring(26, 28)
|
||||
cardIndex.value = key.substring(4, 10)
|
||||
}
|
||||
if (timeData.length >= 32) {
|
||||
val startMin = timeData.substring(10, 12)
|
||||
val startHour = timeData.substring(12, 14)
|
||||
val startDate = timeData.substring(14, 16)
|
||||
val startMonth = timeData.substring(16, 18)
|
||||
val startYear = timeData.substring(18, 20)
|
||||
issuedAt.value = "$startDate/$startMonth/$startYear $startHour:$startMin"
|
||||
|
||||
val endMin = timeData.substring(20, 22)
|
||||
val endHour = timeData.substring(22, 24)
|
||||
val endDate = timeData.substring(24, 26)
|
||||
val endMonth = timeData.substring(26, 28)
|
||||
val endYear = timeData.substring(28, 30)
|
||||
expiresAt.value = "$endDate/$endMonth/$endYear $endHour:$endMin"
|
||||
|
||||
val checkoutTimeLong = Calendar.getInstance().apply {
|
||||
set(Calendar.YEAR, 2000 + endYear.toInt())
|
||||
set(Calendar.MONTH, endMonth.toInt() - 1)
|
||||
set(Calendar.DAY_OF_MONTH, endDate.toInt())
|
||||
set(Calendar.HOUR_OF_DAY, endHour.toInt())
|
||||
set(Calendar.MINUTE, endMin.toInt())
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.time.time
|
||||
expired.value = Date().time > checkoutTimeLong
|
||||
}
|
||||
val authC = mifare.authenticateSectorWithKeyA(3, MifareClassic.KEY_DEFAULT)
|
||||
val authD = mifare.authenticateSectorWithKeyB(3, MifareClassic.KEY_DEFAULT)
|
||||
if (authC || authD) {
|
||||
val issuerBytes = mifare.readBlock(mifare.sectorToBlock(3) + 0)
|
||||
val issuerIdBytes = mifare.readBlock(mifare.sectorToBlock(3) + 1)
|
||||
issuedBy.value = issuerBytes.toString(Charsets.UTF_8).trim()
|
||||
issuedById.value = issuerIdBytes.toString(Charsets.UTF_8).trim()
|
||||
}
|
||||
error.value = null
|
||||
} catch (e: Exception) {
|
||||
error.value = e.localizedMessage ?: "Read failed."
|
||||
} finally {
|
||||
try {
|
||||
mifare.close()
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBlockHex(blockIndex: Int, mifareClassic: MifareClassic): String {
|
||||
val data = mifareClassic.readBlock(blockIndex)
|
||||
return data.joinToString("") { String.format("%02X", it) }
|
||||
}
|
||||
|
||||
private fun writeRevokeTimeData(tag: Tag, timeDataHex: String): String? {
|
||||
val mifare = MifareClassic.get(tag) ?: return "Unsupported card type (not Mifare Classic)."
|
||||
return try {
|
||||
mifare.connect()
|
||||
val sectorIndex = 0
|
||||
val authA = mifare.authenticateSectorWithKeyA(sectorIndex, MifareClassic.KEY_DEFAULT)
|
||||
val authB = mifare.authenticateSectorWithKeyB(sectorIndex, MifareClassic.KEY_DEFAULT)
|
||||
if (!authA && !authB) return "Card authentication failed (sector 0)."
|
||||
val timeBytes = hexToBytes(timeDataHex)
|
||||
if (timeBytes.size != 16) return "Invalid time data size."
|
||||
val blockIndex = mifare.sectorToBlock(sectorIndex)
|
||||
mifare.writeBlock(blockIndex + 2, timeBytes)
|
||||
null
|
||||
} catch (e: Exception) {
|
||||
e.localizedMessage ?: "Write failed."
|
||||
} finally {
|
||||
try {
|
||||
mifare.close()
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun hexToBytes(hex: String): ByteArray {
|
||||
val clean = hex.trim().replace(" ", "")
|
||||
val len = clean.length
|
||||
if (len % 2 != 0) return ByteArray(0)
|
||||
val out = ByteArray(len / 2)
|
||||
var i = 0
|
||||
while (i < len) {
|
||||
out[i / 2] = ((clean.substring(i, i + 2)).toInt(16) and 0xFF).toByte()
|
||||
i += 2
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -16,6 +16,8 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
@@ -23,6 +25,9 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -48,6 +53,7 @@ fun IssueTemporaryCardScreen(
|
||||
val context = LocalContext.current
|
||||
val activity = context as? Activity
|
||||
val nfcAdapter = NfcAdapter.getDefaultAdapter(context)
|
||||
val showSuccessDialog = remember { mutableStateOf(false) }
|
||||
val composition = rememberLottieComposition(
|
||||
LottieCompositionSpec.Asset("scan_nfc_animation.json")
|
||||
)
|
||||
@@ -56,6 +62,17 @@ fun IssueTemporaryCardScreen(
|
||||
iterations = LottieConstants.IterateForever
|
||||
)
|
||||
|
||||
LaunchedEffect(propertyId, roomId) {
|
||||
viewModel.reset()
|
||||
showSuccessDialog.value = false
|
||||
}
|
||||
|
||||
LaunchedEffect(state.lastCardId) {
|
||||
if (!state.lastCardId.isNullOrBlank()) {
|
||||
showSuccessDialog.value = true
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(activity, nfcAdapter, propertyId, roomId) {
|
||||
if (activity != null && nfcAdapter != null) {
|
||||
val flags = NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK
|
||||
@@ -143,4 +160,27 @@ fun IssueTemporaryCardScreen(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (showSuccessDialog.value) {
|
||||
AlertDialog(
|
||||
onDismissRequest = {
|
||||
showSuccessDialog.value = false
|
||||
onBack()
|
||||
},
|
||||
title = { Text("Card issued") },
|
||||
text = {
|
||||
Text("Temporary card issued successfully.")
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
showSuccessDialog.value = false
|
||||
onBack()
|
||||
}
|
||||
) {
|
||||
Text("OK")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,10 @@ class IssueTemporaryCardViewModel : ViewModel() {
|
||||
private val _state = MutableStateFlow(IssueTemporaryCardState())
|
||||
val state: StateFlow<IssueTemporaryCardState> = _state
|
||||
|
||||
fun reset() {
|
||||
_state.value = IssueTemporaryCardState()
|
||||
}
|
||||
|
||||
fun onTagScanned(propertyId: String, roomId: String, tag: Tag) {
|
||||
if (propertyId.isBlank() || roomId.isBlank()) return
|
||||
if (state.value.isProcessing) return
|
||||
|
||||
@@ -18,6 +18,7 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Category
|
||||
import androidx.compose.material.icons.filled.CreditCard
|
||||
import androidx.compose.material.icons.filled.Hotel
|
||||
import androidx.compose.material.icons.filled.Layers
|
||||
import androidx.compose.material.icons.filled.Groups
|
||||
@@ -55,6 +56,7 @@ fun RoomsScreen(
|
||||
onBack: () -> Unit,
|
||||
onAddRoom: () -> Unit,
|
||||
onViewRoomTypes: () -> Unit,
|
||||
onViewCardInfo: () -> Unit,
|
||||
canManageRooms: Boolean,
|
||||
onEditRoom: (com.android.trisolarispms.data.api.model.RoomDto) -> Unit,
|
||||
onIssueTemporaryCard: (com.android.trisolarispms.data.api.model.RoomDto) -> Unit,
|
||||
@@ -84,6 +86,9 @@ fun RoomsScreen(
|
||||
IconButton(onClick = onViewRoomTypes) {
|
||||
Icon(Icons.Default.Category, contentDescription = "Room Types")
|
||||
}
|
||||
IconButton(onClick = onViewCardInfo) {
|
||||
Icon(Icons.Default.CreditCard, contentDescription = "Card Info")
|
||||
}
|
||||
IconButton(onClick = onAddRoom) {
|
||||
Icon(Icons.Default.Add, contentDescription = "Add Room")
|
||||
}
|
||||
@@ -179,7 +184,11 @@ fun RoomsScreen(
|
||||
.alpha(alpha)
|
||||
.combinedClickable(
|
||||
enabled = room.id != null,
|
||||
onClick = { onIssueTemporaryCard(room) },
|
||||
onClick = {
|
||||
if (room.hasNfc != false) {
|
||||
onIssueTemporaryCard(room)
|
||||
}
|
||||
},
|
||||
onLongClick = { onEditRoom(room) }
|
||||
),
|
||||
colors = CardDefaults.cardColors(
|
||||
@@ -255,6 +264,14 @@ fun RoomsScreen(
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
if (room.tempCardActive == true) {
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
Text(
|
||||
text = "Temp card active",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = androidx.compose.ui.graphics.Color(0xFF2E7D32)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user