fixed the issue where login will automatically logout some people, fixed the ui bug

This commit is contained in:
sathwikcs
2024-10-26 18:28:23 +05:30
parent 204a0084da
commit 1efbb9af0f
2 changed files with 471 additions and 429 deletions

View File

@@ -2,11 +2,7 @@ package `in`.sminnovations.attendanceapp.screens.home.ui
import android.Manifest
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -16,10 +12,12 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material3.Button
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -29,264 +27,316 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionState
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import `in`.sminnovations.attendanceapp.screens.home.ui.componets.QRCodeScannerScreen
import `in`.sminnovations.attendanceapp.screens.home.ui.componets.RequestPermissions
import `in`.sminnovations.attendanceapp.screens.home.viewModel.AttendanceUiState
import `in`.sminnovations.attendanceapp.screens.home.viewModel.HomeViewModel
@OptIn(ExperimentalPermissionsApi::class, ExperimentalMaterial3Api::class)
@Composable
fun HomeScreen(viewModel: HomeViewModel = hiltViewModel()) {
val isScanning by viewModel.isScanning.collectAsState()
val qrCodeValue by viewModel.qrCodeValue.collectAsState()
val userName by viewModel.userName.collectAsState()
val checkedOutTime by viewModel.checkedOut.collectAsState()
val isLoading by viewModel.isLoading.collectAsState()
val isCheckedIn by viewModel.isCheckedIn.collectAsState()
val isCheckedOut by viewModel.isCheckedOut.collectAsState()
val errorMessage by viewModel.errorMessage.collectAsState()
fun HomeScreen(
viewModel: HomeViewModel = hiltViewModel(),
modifier: Modifier = Modifier
) {
val uiState by viewModel.uiState.collectAsState()
val cameraPermissionState = rememberPermissionState(Manifest.permission.CAMERA)
AttendanceScreenContent(
uiState = uiState,
cameraPermissionState = cameraPermissionState,
onScannerToggle = { if (uiState.isScanning) viewModel.stopScanning() else viewModel.startScanning() },
onQrCodeScanned = viewModel::setQRCodeValue,
modifier = modifier
)
}
val allPermissionsGranted = remember {
derivedStateOf {
cameraPermissionState.status.isGranted
@OptIn(ExperimentalPermissionsApi::class)
@Composable
private fun AttendanceScreenContent(
uiState: AttendanceUiState,
cameraPermissionState: PermissionState,
onScannerToggle: () -> Unit,
onQrCodeScanned: (String) -> Unit,
modifier: Modifier = Modifier
) {
Box(modifier = modifier.fillMaxSize()) {
when {
!cameraPermissionState.status.isGranted -> {
RequestPermissionsContent(cameraPermissionState)
}
uiState.isScanning -> {
ScannerContent(
onQrCodeScanned = onQrCodeScanned,
onClose = onScannerToggle
)
}
uiState.errorMessage != null -> {
ErrorContent(
errorMessage = uiState.errorMessage,
userName = uiState.userName,
checkedOutTime = uiState.checkedOutTime
)
}
uiState.isCheckedIn -> {
AttendanceStatusCard(
title = "Checked In as",
userName = uiState.userName
)
}
uiState.isCheckedOut -> {
AttendanceStatusCard(
title = "Checked Out as",
userName = uiState.userName
)
}
}
// Loading overlay
if (uiState.isLoading) {
LoadingOverlay()
}
// Bottom scanner toggle button
ScannerToggleButton(
isScanning = uiState.isScanning,
onClick = onScannerToggle,
modifier = Modifier.align(Alignment.BottomCenter)
)
}
}
@OptIn(ExperimentalPermissionsApi::class)
@Composable
private fun RequestPermissionsContent(cameraPermissionState: PermissionState) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = "Camera Permission Required",
style = MaterialTheme.typography.headlineMedium,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { cameraPermissionState.launchPermissionRequest() }) {
Text("Request Permission")
}
}
}
@Composable
private fun ScannerContent(
onQrCodeScanned: (String) -> Unit,
onClose: () -> Unit
) {
Box(modifier = Modifier.fillMaxSize()) {
QRCodeScannerScreen(onQrCodeScanned)
IconButton(
onClick = onClose,
modifier = Modifier
.align(Alignment.TopStart)
.padding(16.dp)
) {
Icon(Icons.Default.ArrowBack, contentDescription = "Close Scanner")
}
}
Box(modifier = Modifier.fillMaxSize()) {
if (!allPermissionsGranted.value) {
RequestPermissions(
cameraPermissionState = cameraPermissionState,
)
} else {
AnimatedVisibility(
visible = isScanning,
enter = expandVertically(expandFrom = Alignment.Bottom) + fadeIn(),
exit = shrinkVertically(shrinkTowards = Alignment.Bottom) + fadeOut()
) {
Box(modifier = Modifier.fillMaxSize()) {
QRCodeScannerScreen(viewModel::setQRCodeValue)
IconButton(
onClick = { viewModel.stopScanning() },
modifier = Modifier
.align(Alignment.TopStart)
.padding(16.dp)
) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
}
}
BackHandler { onClose() }
}
@Composable
private fun MainContent(uiState: AttendanceUiState) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
when {
uiState.errorMessage != null -> {
ErrorContent(
errorMessage = uiState.errorMessage,
userName = uiState.userName,
checkedOutTime = uiState.checkedOutTime
)
}
AnimatedVisibility(
visible = !isScanning && qrCodeValue == "" && isCheckedIn && !isCheckedOut,
enter = expandVertically(expandFrom = Alignment.Bottom) + fadeIn(),
exit = shrinkVertically(shrinkTowards = Alignment.Bottom) + fadeOut()
) {
Box(
contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()
) {
Column {
OutlinedCard {
Text(
text = "Checked In as",
letterSpacing = 2.sp,
lineHeight = 40.sp,
fontSize = 30.sp,
color = Color.Green,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(10.dp, 5.dp)
)
}
Spacer(modifier = Modifier.height(10.dp))
Text(
text = " $userName",
letterSpacing = 2.sp,
lineHeight = 40.sp,
fontSize = 30.sp,
fontWeight = FontWeight.Bold
)
}
}
uiState.isCheckedIn -> {
AttendanceStatusCard(
title = "Checked In as",
userName = uiState.userName
)
}
AnimatedVisibility(
visible = !isScanning && qrCodeValue == "" && !isCheckedIn && isCheckedOut,
enter = expandVertically(expandFrom = Alignment.Bottom) + fadeIn(),
exit = shrinkVertically(shrinkTowards = Alignment.Bottom) + fadeOut()
) {
Box(
contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()
) {
Column {
OutlinedCard {
Text(
text = "Checked Out as",
letterSpacing = 2.sp,
lineHeight = 40.sp,
fontSize = 30.sp,
color = Color.Green,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(10.dp, 5.dp)
)
}
Spacer(modifier = Modifier.height(10.dp))
Text(
text = " $userName",
letterSpacing = 2.sp,
lineHeight = 40.sp,
fontSize = 30.sp,
fontWeight = FontWeight.Bold
)
}
}
uiState.isCheckedOut -> {
AttendanceStatusCard(
title = "Checked Out as",
userName = uiState.userName
)
}
}
}
}
errorMessage?.let { message ->
AnimatedVisibility(
visible = message != "" && message != null,
enter = expandVertically(expandFrom = Alignment.Bottom) + fadeIn(),
exit = shrinkVertically(shrinkTowards = Alignment.Bottom) + fadeOut()
) {
Box(
contentAlignment = Alignment.TopCenter,
modifier = Modifier
.fillMaxSize()
.padding(20.dp)
) {
Column {
if (userName != null && userName != "") {
Text(
text = buildAnnotatedString {
append("This Error Message is for ")
withStyle(style = SpanStyle(color = Color.Green)) {
append(userName)
}
},
color = MaterialTheme.colorScheme.error,
fontWeight = FontWeight.Bold,
letterSpacing = 2.sp,
lineHeight = 40.sp,
fontSize = 20.sp,
modifier = Modifier.padding(8.dp)
)
}
OutlinedCard(
modifier = Modifier
.wrapContentSize()
.padding(16.dp)
) {
Text(
text = message,
letterSpacing = 2.sp,
lineHeight = 40.sp,
fontSize = 30.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(10.dp, 7.dp),
color = MaterialTheme.colorScheme.error
)
}
if (checkedOutTime !="") {
Spacer(modifier = Modifier.height(15.dp))
Text(
text = "check Out Time",
lineHeight = 40.sp,
fontSize = 30.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(15.dp))
Text(
text = checkedOutTime,
lineHeight = 40.sp,
fontSize = 30.sp,
fontWeight = FontWeight.Bold,
color = Color.Green
)
}
}
}
}
}
// Loading Indicator
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center)
@Composable
private fun AttendanceStatusCard(
title: String,
userName: String?
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
OutlinedCard {
Text(
text = title,
letterSpacing = 2.sp,
lineHeight = 40.sp,
fontSize = 30.sp,
color = Color.Green,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(10.dp, 5.dp)
)
}
OutlinedCard(modifier = Modifier
Spacer(modifier = Modifier.height(10.dp))
Text(
text = userName ?: "",
letterSpacing = 2.sp,
lineHeight = 40.sp,
fontSize = 30.sp,
fontWeight = FontWeight.Bold
)
}
}
@Composable
private fun ErrorContent(
errorMessage: String,
userName: String?,
checkedOutTime: String
) {
Column(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.align(Alignment.BottomCenter)
.padding(10.dp), onClick ={
if (isScanning) {
viewModel.stopScanning()
} else {
viewModel.startScanning()
.padding(20.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Only show the "Error Message for" text if we have a valid userName
if (!userName.isNullOrEmpty()) {
OutlinedCard(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp)
) {
Text(
text = buildAnnotatedString {
append("Error Message for ")
withStyle(style = SpanStyle(color = Color.Green)) {
append(userName)
}
},
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(16.dp)
)
}
},
colors = CardDefaults.cardColors(MaterialTheme.colorScheme.primary)
}
OutlinedCard(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = if (checkedOutTime.isNotEmpty()) 16.dp else 0.dp)
) {
Box(modifier = Modifier.fillMaxWidth()
,
Text(
text = errorMessage,
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.error,
modifier = Modifier.padding(16.dp)
)
}
{
Column(Modifier.align(Alignment.BottomCenter)) {
Row(
Modifier
.wrapContentHeight()
.fillMaxWidth()
.padding(bottom = 24.dp, top = 24.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = if (isScanning) "Close Scanner" else "Open Scanner",
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
modifier =Modifier.padding(20.dp,10.dp)
)
}
LaunchedEffect(qrCodeValue) {
viewModel.login()
}
if (isScanning) {
BackHandler { viewModel.stopScanning() }
}
if (checkedOutTime.isNotEmpty()) {
OutlinedCard(
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Check Out Time",
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = checkedOutTime,
style = MaterialTheme.typography.titleLarge,
color = Color.Green
)
}
}
}
}
}
@Composable
private fun LoadingOverlay() {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 0.3f)),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(48.dp)
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ScannerToggleButton(
isScanning: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
OutlinedCard(
onClick = onClick,
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primary
),
modifier = modifier
.fillMaxWidth()
.padding(16.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = if (isScanning) Icons.Default.Close else Icons.Default.KeyboardArrowUp,
contentDescription = null,
modifier = Modifier.padding(end = 8.dp)
)
Text(
text = if (isScanning) "Close Scanner" else "Open Scanner",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
}
}
}

View File

@@ -6,247 +6,239 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.firebase.Timestamp
import com.google.firebase.firestore.DocumentSnapshot
import com.google.firebase.firestore.FirebaseFirestore
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.tasks.await
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import javax.inject.Inject
@HiltViewModel
class HomeViewModel @Inject constructor(
private val firebaseFirestore: FirebaseFirestore,
) : ViewModel() {
private val _qrCodeValue = MutableStateFlow<String?>(null)
val qrCodeValue: StateFlow<String?> = _qrCodeValue.asStateFlow()
private val _isScanning = MutableStateFlow(false)
val isScanning: StateFlow<Boolean> = _isScanning.asStateFlow()
private val _userName = MutableStateFlow<String?>(null)
val userName: StateFlow<String?> = _userName.asStateFlow()
private val _isCheckedIn = MutableStateFlow(false)
val isCheckedIn: StateFlow<Boolean> = _isCheckedIn.asStateFlow()
private val _isCheckedOut = MutableStateFlow(false)
val isCheckedOut: StateFlow<Boolean> = _isCheckedOut.asStateFlow()
private val _checkedOutTime = MutableStateFlow("")
val checkedOut: StateFlow<String> = _checkedOutTime.asStateFlow()
private val _errorMessage = MutableStateFlow<String?>(null)
val errorMessage: StateFlow<String?> = _errorMessage.asStateFlow()
private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
private fun postErrorMessage(message: String) {
viewModelScope.launch {
_errorMessage.emit(message)
}
}
fun startScanning() {
_isScanning.value = true
_errorMessage.value = null
_isCheckedIn.value = false
_isCheckedOut.value = false
putQRvalueEmpty()
}
fun stopScanning() {
_isScanning.value = false
_errorMessage.value = null
_isCheckedIn.value = false
_isCheckedOut.value = false
_checkedOutTime.value = ""
}
fun setQRCodeValue(value: String) {
viewModelScope.launch {
_qrCodeValue.emit(value)
stopScanning()
}
}
private fun formatTimestamp(timestamp: Timestamp): String {
val sdf =
SimpleDateFormat("dd MMMM yyyy hh:mm:ss a", Locale.getDefault()) // Format with AM/PM
return sdf.format(timestamp.toDate())
}
private val _uiState = MutableStateFlow(AttendanceUiState())
val uiState: StateFlow<AttendanceUiState> = _uiState.asStateFlow()
init {
viewModelScope.launch {
startScanning()
}
}
private fun putCheckInAttendance(id: String) {
fun startScanning() {
viewModelScope.launch {
_isLoading.value = true
val currentDate = SimpleDateFormat("dd-MM-yyyy", Locale.getDefault()).format(Date())
val attendanceRef = firebaseFirestore.collection("attendance_$id").document(currentDate)
_uiState.update { currentState ->
AttendanceUiState( // Reset to completely fresh state
isScanning = true
)
}
}
}
attendanceRef.get().addOnSuccessListener { document ->
_isLoading.value = false
if (!document.exists()) {
val checkInData = hashMapOf(
"DayCompleted" to false,
"checkIn" to formatTimestamp(Timestamp.now()),
"checkOut" to "NA"
fun stopScanning() {
viewModelScope.launch {
_uiState.update { currentState ->
currentState.copy(
isScanning = false,
errorMessage = null,
qrCodeValue = null
)
}
}
}
fun setQRCodeValue(value: String) {
viewModelScope.launch {
_uiState.update {
it.copy(
qrCodeValue = value,
userName = null, // Reset userName when new QR code is scanned
errorMessage = null
)
}
stopScanning()
processQRCode(value)
}
}
private fun processQRCode(userId: String) {
if (userId.isBlank()) return
viewModelScope.launch {
try {
setLoading(true)
_uiState.update {
it.copy(
errorMessage = null,
userName = null // Reset userName before processing
)
attendanceRef.set(checkInData).addOnSuccessListener {
Log.d("Firestore", "Check-in successful")
viewModelScope.launch {
_qrCodeValue.value = ""
_isCheckedIn.value =true
}
}.addOnFailureListener { e ->
postErrorMessage("Error adding check-in: ${e.message}")
}
} else {
putCheckOutAttendance(id)
}
}.addOnFailureListener { exception ->
_isLoading.value = false
postErrorMessage("Error fetching document: ${exception.message}")
}
}
}
private fun putCheckOutAttendance(id: String) {
viewModelScope.launch {
_isLoading.value = true
val currentDate = SimpleDateFormat("dd-MM-yyyy", Locale.getDefault()).format(Date())
val attendanceRef = firebaseFirestore.collection("attendance_$id").document(currentDate)
attendanceRef.get().addOnSuccessListener { document ->
_isLoading.value = false
if (document.exists()) {
val checkIn = document.getString("checkIn")
val checkOut = document.getString("checkOut")
if (checkIn != null && checkOut == "NA") {
val checkOutData = hashMapOf(
"checkOut" to formatTimestamp(Timestamp.now()), "DayCompleted" to true
) as Map<String, Any>
attendanceRef.update(checkOutData).addOnSuccessListener {
viewModelScope.launch {
_isCheckedOut.value =true
}
Log.d("Firestore", "Check-out successful")
}.addOnFailureListener { e ->
postErrorMessage("Error adding check-out: ${e.message}")
}
} else if (checkOut != "NA") {
postErrorMessage("Already checked out for today.")
_checkedOutTime.value = checkOut!!
} else {
putCheckInAttendance(id)
}
} else {
postErrorMessage("No attendance found for today. Please check in first.")
val employeeDoc = getEmployeeDocument(userId)
if (employeeDoc == null) {
handleError("Invalid employee ID")
return@launch
}
}.addOnFailureListener { exception ->
_isLoading.value = false
postErrorMessage("Error fetching document: ${exception.message}")
}
}
}
fun login(userId: String? = qrCodeValue.value) {
if (qrCodeValue.value != "" && userId != null) { // don't simplify it
_isLoading.value = true
viewModelScope.launch {
_errorMessage.emit(null)
firebaseFirestore.collection("employees").whereEqualTo("Id", userId).get()
.addOnSuccessListener {
if (!it.isEmpty) {
for (doc in it) {
if (true == doc.getBoolean("EmployeeStatus")) {
viewModelScope.launch {
_isLoading.value = false
_userName.value = doc.getString("Name")
putQRvalueEmpty()
putCheckInAttendance(userId)
}
} else {
postErrorMessage("ID has been expired }")
}
}
} else {
_isLoading.value = false
postErrorMessage("Error fetching ID }")
}
}.addOnFailureListener {
_isLoading.value = false
postErrorMessage("Error FailureListener}")
if (!employeeDoc.getBoolean("EmployeeStatus")!!) {
// Set userName only for expired ID case
_uiState.update {
it.copy(
userName = employeeDoc.getString("Name"),
errorMessage = "Employee ID has expired"
)
}
}
} else {
return@launch
}
_uiState.update { it.copy(userName = employeeDoc.getString("Name")) }
val currentDate = getCurrentFormattedDate()
val attendanceDoc = getAttendanceDocument(userId, currentDate)
if (attendanceDoc?.exists() == true) {
handleExistingAttendance(userId, attendanceDoc)
} else {
handleNewAttendance(userId, currentDate)
}
} catch (e: Exception) {
handleError("An error occurred: ${e.message}")
} finally {
setLoading(false)
}
}
}
fun logout(userId: String? = qrCodeValue.value) {
if (qrCodeValue.value != "" && userId != null) {
viewModelScope.launch {
_errorMessage.emit(null)
firebaseFirestore.collection("employees").whereEqualTo("Id", userId).get()
.addOnSuccessListener {
if (!it.isEmpty) {
for (doc in it) {
if (true == doc.getBoolean("EmployeeStatus")) {
viewModelScope.launch {
_userName.value =doc.getString("Name")
putQRvalueEmpty()
putCheckOutAttendance(userId)
}
} else {
_isLoading.value = false
postErrorMessage("ID has been expired }")
}
}
} else {
_isLoading.value = false
postErrorMessage("Error fetching ID }")
}
}.addOnFailureListener {
_isLoading.value = false
postErrorMessage("Error FailureListener}")
}
}
} else {
}
}
private fun putQRvalueEmpty() {
private fun handleError(message: String) {
viewModelScope.launch {
_qrCodeValue.value = ""
_isLoading.value =false
_checkedOutTime.value = ""
_uiState.update {
it.copy(
errorMessage = message,
isLoading = false,
userName = null // Reset userName on error
)
}
}
}
private suspend fun handleExistingAttendance(userId: String, document: DocumentSnapshot) {
val checkOut = document.getString("checkOut")
when {
checkOut == "NA" -> processCheckOut(userId, document)
checkOut != null -> {
_uiState.update {
it.copy(
checkedOutTime = checkOut,
errorMessage = "Already checked out for today."
)
}
}
else -> handleError("Invalid attendance record")
}
}
private suspend fun handleNewAttendance(userId: String, currentDate: String) {
try {
val checkInData = hashMapOf(
"DayCompleted" to false,
"checkIn" to formatTimestamp(Timestamp.now()),
"checkOut" to "NA"
)
firebaseFirestore.collection("attendance_$userId")
.document(currentDate)
.set(checkInData)
.await()
_uiState.update {
it.copy(
isCheckedIn = true,
qrCodeValue = null
)
}
} catch (e: Exception) {
handleError("Failed to check in: ${e.message}")
}
}
private suspend fun processCheckOut(userId: String, document: DocumentSnapshot) {
try {
val checkOutData = hashMapOf(
"checkOut" to formatTimestamp(Timestamp.now()),
"DayCompleted" to true
)
document.reference.update(checkOutData as Map<String, Any>).await()
_uiState.update {
it.copy(
isCheckedOut = true,
qrCodeValue = null
)
}
} catch (e: Exception) {
handleError("Failed to check out: ${e.message}")
}
}
private suspend fun getEmployeeDocument(userId: String): DocumentSnapshot? {
return try {
val querySnapshot = firebaseFirestore.collection("employees")
.whereEqualTo("Id", userId)
.get()
.await()
if (querySnapshot.isEmpty) null else querySnapshot.documents.first()
} catch (e: Exception) {
throw Exception("Failed to fetch employee data")
}
}
private suspend fun getAttendanceDocument(userId: String, date: String): DocumentSnapshot? {
return try {
firebaseFirestore.collection("attendance_$userId")
.document(date)
.get()
.await()
} catch (e: Exception) {
throw Exception("Failed to fetch attendance data")
}
}
private fun setLoading(loading: Boolean) {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = loading) }
}
}
private fun getCurrentFormattedDate(): String {
return SimpleDateFormat("dd-MM-yyyy", Locale.getDefault()).format(Date())
}
private fun formatTimestamp(timestamp: Timestamp): String {
return SimpleDateFormat("dd MMMM yyyy hh:mm:ss a", Locale.getDefault())
.format(timestamp.toDate())
}
}
data class AttendanceUiState(
val isScanning: Boolean = false,
val qrCodeValue: String? = null,
val userName: String? = null,
val isCheckedIn: Boolean = false,
val isCheckedOut: Boolean = false,
val checkedOutTime: String = "",
val errorMessage: String? = null,
val isLoading: Boolean = false
)