From 1efbb9af0f7458343d11ece98ab31e6afff4443a Mon Sep 17 00:00:00 2001 From: sathwikcs <85106054+sathwikcs@users.noreply.github.com> Date: Sat, 26 Oct 2024 18:28:23 +0530 Subject: [PATCH] fixed the issue where login will automatically logout some people, fixed the ui bug --- .../attendanceapp/screens/home/ui/home.kt | 500 ++++++++++-------- .../screens/home/viewModel/homeVM.kt | 400 +++++++------- 2 files changed, 471 insertions(+), 429 deletions(-) diff --git a/app/src/main/java/in/sminnovations/attendanceapp/screens/home/ui/home.kt b/app/src/main/java/in/sminnovations/attendanceapp/screens/home/ui/home.kt index 896b6af..42f1f50 100644 --- a/app/src/main/java/in/sminnovations/attendanceapp/screens/home/ui/home.kt +++ b/app/src/main/java/in/sminnovations/attendanceapp/screens/home/ui/home.kt @@ -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 + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/in/sminnovations/attendanceapp/screens/home/viewModel/homeVM.kt b/app/src/main/java/in/sminnovations/attendanceapp/screens/home/viewModel/homeVM.kt index 3e23f6f..c260bb3 100644 --- a/app/src/main/java/in/sminnovations/attendanceapp/screens/home/viewModel/homeVM.kt +++ b/app/src/main/java/in/sminnovations/attendanceapp/screens/home/viewModel/homeVM.kt @@ -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(null) - val qrCodeValue: StateFlow = _qrCodeValue.asStateFlow() - - private val _isScanning = MutableStateFlow(false) - val isScanning: StateFlow = _isScanning.asStateFlow() - - private val _userName = MutableStateFlow(null) - val userName: StateFlow = _userName.asStateFlow() - - - private val _isCheckedIn = MutableStateFlow(false) - val isCheckedIn: StateFlow = _isCheckedIn.asStateFlow() - - private val _isCheckedOut = MutableStateFlow(false) - val isCheckedOut: StateFlow = _isCheckedOut.asStateFlow() - - private val _checkedOutTime = MutableStateFlow("") - val checkedOut: StateFlow = _checkedOutTime.asStateFlow() - - - private val _errorMessage = MutableStateFlow(null) - val errorMessage: StateFlow = _errorMessage.asStateFlow() - - private val _isLoading = MutableStateFlow(false) - val isLoading: StateFlow = _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 = _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 - - 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).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 +) \ No newline at end of file