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

View File

@@ -6,247 +6,239 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.firebase.Timestamp import com.google.firebase.Timestamp
import com.google.firebase.firestore.DocumentSnapshot
import com.google.firebase.firestore.FirebaseFirestore import com.google.firebase.firestore.FirebaseFirestore
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.tasks.await
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class HomeViewModel @Inject constructor( class HomeViewModel @Inject constructor(
private val firebaseFirestore: FirebaseFirestore, private val firebaseFirestore: FirebaseFirestore,
) : ViewModel() { ) : ViewModel() {
private val _qrCodeValue = MutableStateFlow<String?>(null) private val _uiState = MutableStateFlow(AttendanceUiState())
val qrCodeValue: StateFlow<String?> = _qrCodeValue.asStateFlow() val uiState: StateFlow<AttendanceUiState> = _uiState.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())
}
init { init {
viewModelScope.launch { viewModelScope.launch {
startScanning() startScanning()
} }
} }
private fun putCheckInAttendance(id: String) { fun startScanning() {
viewModelScope.launch { viewModelScope.launch {
_isLoading.value = true _uiState.update { currentState ->
val currentDate = SimpleDateFormat("dd-MM-yyyy", Locale.getDefault()).format(Date()) AttendanceUiState( // Reset to completely fresh state
val attendanceRef = firebaseFirestore.collection("attendance_$id").document(currentDate) isScanning = true
)
}
}
}
attendanceRef.get().addOnSuccessListener { document -> fun stopScanning() {
_isLoading.value = false viewModelScope.launch {
if (!document.exists()) { _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
)
}
val employeeDoc = getEmployeeDocument(userId)
if (employeeDoc == null) {
handleError("Invalid employee ID")
return@launch
}
if (!employeeDoc.getBoolean("EmployeeStatus")!!) {
// Set userName only for expired ID case
_uiState.update {
it.copy(
userName = employeeDoc.getString("Name"),
errorMessage = "Employee ID has expired"
)
}
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)
}
}
}
private fun handleError(message: String) {
viewModelScope.launch {
_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( val checkInData = hashMapOf(
"DayCompleted" to false, "DayCompleted" to false,
"checkIn" to formatTimestamp(Timestamp.now()), "checkIn" to formatTimestamp(Timestamp.now()),
"checkOut" to "NA" "checkOut" to "NA"
) )
attendanceRef.set(checkInData).addOnSuccessListener { firebaseFirestore.collection("attendance_$userId")
Log.d("Firestore", "Check-in successful") .document(currentDate)
.set(checkInData)
.await()
viewModelScope.launch { _uiState.update {
_qrCodeValue.value = "" it.copy(
_isCheckedIn.value =true isCheckedIn = true,
} qrCodeValue = null
)
}.addOnFailureListener { e ->
postErrorMessage("Error adding check-in: ${e.message}")
}
} else {
putCheckOutAttendance(id)
}
}.addOnFailureListener { exception ->
_isLoading.value = false
postErrorMessage("Error fetching document: ${exception.message}")
} }
} catch (e: Exception) {
handleError("Failed to check in: ${e.message}")
} }
} }
private fun putCheckOutAttendance(id: String) { private suspend fun processCheckOut(userId: String, document: DocumentSnapshot) {
viewModelScope.launch { try {
_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( val checkOutData = hashMapOf(
"checkOut" to formatTimestamp(Timestamp.now()), "DayCompleted" to true "checkOut" to formatTimestamp(Timestamp.now()),
) as Map<String, Any> "DayCompleted" to true
)
attendanceRef.update(checkOutData).addOnSuccessListener { 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 { viewModelScope.launch {
_isCheckedOut.value =true _uiState.update { it.copy(isLoading = loading) }
}
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.")
}
}.addOnFailureListener { exception ->
_isLoading.value = false
postErrorMessage("Error fetching document: ${exception.message}")
}
} }
} }
fun login(userId: String? = qrCodeValue.value) { private fun getCurrentFormattedDate(): String {
if (qrCodeValue.value != "" && userId != null) { // don't simplify it return SimpleDateFormat("dd-MM-yyyy", Locale.getDefault()).format(Date())
_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}")
}
}
} else {
private fun formatTimestamp(timestamp: Timestamp): String {
return SimpleDateFormat("dd MMMM yyyy hh:mm:ss a", Locale.getDefault())
.format(timestamp.toDate())
} }
} }
fun logout(userId: String? = qrCodeValue.value) { data class AttendanceUiState(
if (qrCodeValue.value != "" && userId != null) { val isScanning: Boolean = false,
viewModelScope.launch { val qrCodeValue: String? = null,
_errorMessage.emit(null) val userName: String? = null,
firebaseFirestore.collection("employees").whereEqualTo("Id", userId).get() val isCheckedIn: Boolean = false,
.addOnSuccessListener { val isCheckedOut: Boolean = false,
if (!it.isEmpty) { val checkedOutTime: String = "",
for (doc in it) { val errorMessage: String? = null,
if (true == doc.getBoolean("EmployeeStatus")) { val isLoading: Boolean = false
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() {
viewModelScope.launch {
_qrCodeValue.value = ""
_isLoading.value =false
_checkedOutTime.value = ""
}
}
}