removed the unnecessary things
This commit is contained in:
@@ -1,41 +0,0 @@
|
||||
package `in`.sminnovations.attendanceapp
|
||||
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import `in`.sminnovations.attendanceapp.screens.login.ui.LoginScreen
|
||||
|
||||
|
||||
@Composable
|
||||
fun AppNavigation() {
|
||||
val navController = rememberNavController()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||||
NavigationHost(navController = navController)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O_MR1)
|
||||
@Composable
|
||||
fun NavigationHost(navController: NavHostController) {
|
||||
NavHost(navController = navController, startDestination = "login") {
|
||||
composable("login") {
|
||||
LoginScreen(
|
||||
loginViewModel = hiltViewModel(),
|
||||
onLoginSuccess = {
|
||||
navController.navigate("main") {
|
||||
popUpTo("login") { inclusive = true }
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
composable("main") {
|
||||
MainScreen(navController)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package `in`.sminnovations.attendanceapp.data.dataStore
|
||||
|
||||
data class Employee(
|
||||
val androidID: String = "",
|
||||
val dateOfJoining: String = "",
|
||||
val dateOfLeaving: String = "",
|
||||
val designation: String = "Android Developer",
|
||||
val employeeStatus: Boolean = true,
|
||||
val id: String = "",
|
||||
val name: String = "",
|
||||
val password: String = "",
|
||||
val photo: String = "",
|
||||
val dateOfBirth: String = ""
|
||||
)
|
||||
@@ -1,17 +0,0 @@
|
||||
package `in`.sminnovations.attendanceapp.data.dataStore
|
||||
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
|
||||
object EmployeePreferencesKeys {
|
||||
val ANDROID_ID = stringPreferencesKey("android_id")
|
||||
val DATE_OF_JOINING = stringPreferencesKey("date_of_joining")
|
||||
val DATE_OF_LEAVING = stringPreferencesKey("date_of_leaving")
|
||||
val DESIGNATION = stringPreferencesKey("designation")
|
||||
val EMPLOYEE_STATUS = booleanPreferencesKey("employee_status")
|
||||
val ID = stringPreferencesKey("id")
|
||||
val NAME = stringPreferencesKey("name")
|
||||
val PASSWORD = stringPreferencesKey("password")
|
||||
val PHOTO = stringPreferencesKey("photo")
|
||||
val DATE_OF_BIRTH = stringPreferencesKey("date_of_birth")
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
package `in`.sminnovations.attendanceapp.data.dataStore
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.emptyPreferences
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import `in`.sminnovations.attendanceapp.screens.settings.data.EmployeeDataSettings
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
||||
val Context.employeeDataStore by preferencesDataStore("employee_data")
|
||||
|
||||
@Singleton
|
||||
class EmployeeRepository @Inject constructor(
|
||||
@ApplicationContext private val context: Context
|
||||
) {
|
||||
suspend fun saveEmployeeData(employee: Employee) {
|
||||
context.employeeDataStore.edit { preferences ->
|
||||
preferences[EmployeePreferencesKeys.ANDROID_ID] = employee.androidID
|
||||
preferences[EmployeePreferencesKeys.DATE_OF_JOINING] = employee.dateOfJoining
|
||||
preferences[EmployeePreferencesKeys.DATE_OF_LEAVING] = employee.dateOfLeaving
|
||||
preferences[EmployeePreferencesKeys.DESIGNATION] = employee.designation
|
||||
preferences[EmployeePreferencesKeys.EMPLOYEE_STATUS] = employee.employeeStatus
|
||||
preferences[EmployeePreferencesKeys.ID] = employee.id
|
||||
preferences[EmployeePreferencesKeys.NAME] = employee.name
|
||||
preferences[EmployeePreferencesKeys.PASSWORD] = employee.password
|
||||
preferences[EmployeePreferencesKeys.PHOTO] = employee.photo
|
||||
preferences[EmployeePreferencesKeys.DATE_OF_BIRTH] = employee.dateOfBirth
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun getEmployeeData(): Flow<Employee> {
|
||||
return context.employeeDataStore.data
|
||||
.catch { exception ->
|
||||
// If an error occurs, emit an empty preferences object
|
||||
if (exception is IOException) {
|
||||
emit(emptyPreferences())
|
||||
} else {
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
.map { preferences ->
|
||||
Employee(
|
||||
androidID = preferences[EmployeePreferencesKeys.ANDROID_ID] ?: "",
|
||||
dateOfJoining = preferences[EmployeePreferencesKeys.DATE_OF_JOINING] ?: "",
|
||||
dateOfLeaving = preferences[EmployeePreferencesKeys.DATE_OF_LEAVING] ?: "",
|
||||
designation = preferences[EmployeePreferencesKeys.DESIGNATION] ?: "Android Developer",
|
||||
employeeStatus = preferences[EmployeePreferencesKeys.EMPLOYEE_STATUS] ?: true,
|
||||
id = preferences[EmployeePreferencesKeys.ID] ?: "",
|
||||
name = preferences[EmployeePreferencesKeys.NAME] ?: "",
|
||||
password = preferences[EmployeePreferencesKeys.PASSWORD] ?: "",
|
||||
photo = preferences[EmployeePreferencesKeys.PHOTO] ?: "",
|
||||
dateOfBirth = preferences[EmployeePreferencesKeys.DATE_OF_BIRTH] ?: ""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun getEmployeeDataSettings(): Flow<EmployeeDataSettings> {
|
||||
return context.employeeDataStore.data
|
||||
.catch { exception ->
|
||||
if (exception is IOException) {
|
||||
emit(emptyPreferences())
|
||||
} else {
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
.map { preferences ->
|
||||
EmployeeDataSettings(
|
||||
name = preferences[EmployeePreferencesKeys.NAME] ?: "",
|
||||
id = preferences[EmployeePreferencesKeys.ID] ?: "",
|
||||
designation = preferences[EmployeePreferencesKeys.DESIGNATION] ?: ""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.history.components
|
||||
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import `in`.sminnovations.attendanceapp.screens.history.data.AttendanceData
|
||||
|
||||
|
||||
@Composable
|
||||
fun EnhancedAttendanceCard(attendance: AttendanceData) {
|
||||
var isExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp, horizontal = 16.dp)
|
||||
.animateContentSize(), // Animate the size change for a smooth expand/collapse
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
elevation = CardDefaults.cardElevation(6.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(MaterialTheme.colorScheme.primaryContainer)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = attendance.date,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 18.sp,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
IconButton(
|
||||
onClick = { isExpanded = !isExpanded }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowDropDown,
|
||||
contentDescription = "Expand or Collapse",
|
||||
tint = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
}
|
||||
}
|
||||
if (isExpanded) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = "Check In: ${attendance.checkInTime ?: "N/A"}",
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
Text(
|
||||
text = "Check Out: ${attendance.checkOutTime ?: "N/A"}",
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.history.data
|
||||
|
||||
import com.google.firebase.Timestamp
|
||||
|
||||
data class AttendanceData(
|
||||
val date: String,
|
||||
val checkInTime: String?,
|
||||
val checkOutTime: String?
|
||||
)
|
||||
@@ -1,62 +0,0 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.history.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import `in`.sminnovations.attendanceapp.screens.history.components.EnhancedAttendanceCard
|
||||
import `in`.sminnovations.attendanceapp.screens.history.viewModel.HistoryViewModel
|
||||
import `in`.sminnovations.attendanceapp.screens.login.viewModel.LoginViewModel
|
||||
|
||||
|
||||
@Composable
|
||||
fun HistoryScreen(
|
||||
historyViewModel: HistoryViewModel = hiltViewModel(),
|
||||
) {
|
||||
val attendanceData = historyViewModel.listOfAttendanceDates.collectAsState().value
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
Row (
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
){
|
||||
Text(
|
||||
text = "Your Attendance History",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier
|
||||
.padding(16.dp),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Button(onClick = {historyViewModel.refresh() }) {
|
||||
Text(text = "Refresh")
|
||||
}
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(vertical = 8.dp)
|
||||
) {
|
||||
items(attendanceData) { attendance ->
|
||||
EnhancedAttendanceCard(attendance = attendance)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.history.viewModel
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.google.firebase.Timestamp
|
||||
import com.google.firebase.firestore.FirebaseFirestore
|
||||
import com.google.firebase.firestore.toObject
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import `in`.sminnovations.attendanceapp.screens.history.data.AttendanceData
|
||||
import `in`.sminnovations.attendanceapp.screens.login.data.LoginDataStore
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class HistoryViewModel @Inject constructor(
|
||||
private val loginDataStore: LoginDataStore,
|
||||
private val firebaseFirestore: FirebaseFirestore,
|
||||
) : ViewModel() {
|
||||
|
||||
private val _isLoggedIn = MutableStateFlow(false)
|
||||
val isLoggedIn = _isLoggedIn.asStateFlow()
|
||||
|
||||
|
||||
private val _listOfAttendanceDates = MutableStateFlow<List<AttendanceData>>(emptyList())
|
||||
val listOfAttendanceDates = _listOfAttendanceDates.asStateFlow()
|
||||
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
getAttendanceData()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun refresh() {
|
||||
viewModelScope.launch {
|
||||
getAttendanceData()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private fun getAttendanceData() {
|
||||
viewModelScope.launch {
|
||||
loginDataStore.loginId.collect { id ->
|
||||
firebaseFirestore.collection("attendance_$id").get()
|
||||
.addOnSuccessListener { result ->
|
||||
viewModelScope.launch {
|
||||
val attendanceList = result.documents.mapNotNull { document ->
|
||||
val date = document.id // Assuming the document ID is the date
|
||||
|
||||
val checkInTimestamp = document.getString("checkIn") ?: "NA"
|
||||
val checkOutTimestamp = document.getString("checkOut") ?: "NA"
|
||||
|
||||
val checkIn = formatTimestamp(checkInTimestamp)
|
||||
val checkOut = formatTimestamp(checkOutTimestamp)
|
||||
|
||||
AttendanceData(date, checkIn, checkOut)
|
||||
}
|
||||
_listOfAttendanceDates.value = attendanceList
|
||||
}
|
||||
}
|
||||
.addOnFailureListener { exception ->
|
||||
Log.e("Firestore", "Error getting documents: ", exception)
|
||||
viewModelScope.launch {
|
||||
_listOfAttendanceDates.value = emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun formatTimestamp(timestamp: Any): String {
|
||||
val sdf = SimpleDateFormat("dd MMMM yyyy hh:mm a", Locale.getDefault())
|
||||
return if (timestamp is Timestamp) {
|
||||
sdf.format(timestamp.toDate())
|
||||
} else {
|
||||
timestamp.toString()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.home.data
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
private const val DATASTORE_NAME = "check_in_check_out_prefs"
|
||||
private val Context.dataStore by preferencesDataStore(name = DATASTORE_NAME)
|
||||
|
||||
@Singleton
|
||||
class CheckInCheckOutDataStore @Inject constructor(@ApplicationContext context: Context) {
|
||||
|
||||
private val dataStore = context.dataStore
|
||||
|
||||
private val CHECK_IN_KEY = stringPreferencesKey("check_in_id")
|
||||
private val CHECK_OUT_KEY = stringPreferencesKey("check_out_id")
|
||||
private val CHECK_IN_Date = stringPreferencesKey("check_in_date_id")
|
||||
private val IS_CHECK_IN = booleanPreferencesKey("is_checked_in_id")
|
||||
private val IS_CHECK_OUT = booleanPreferencesKey("is_checked_in_id")
|
||||
|
||||
|
||||
val checkInId: Flow<String> = dataStore.data.map { preferences ->
|
||||
preferences[CHECK_IN_KEY] ?: ""
|
||||
}
|
||||
|
||||
val checkOutId: Flow<String> = dataStore.data.map { preferences ->
|
||||
preferences[CHECK_OUT_KEY] ?: ""
|
||||
}
|
||||
|
||||
val checkInDate: Flow<String> = dataStore.data.map { preferences ->
|
||||
preferences[CHECK_IN_Date] ?: ""
|
||||
}
|
||||
|
||||
|
||||
|
||||
suspend fun saveLoginData(checkOut: String) {
|
||||
dataStore.edit { preferences ->
|
||||
preferences[CHECK_OUT_KEY] = checkOut
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
suspend fun saveLoginDate(checkDate: String,checkInTime: String) {
|
||||
dataStore.edit { preferences ->
|
||||
preferences[CHECK_IN_Date] = checkDate
|
||||
preferences[CHECK_IN_KEY] = checkInTime
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun saveIsLogIn(isCheckIn: Boolean) {
|
||||
dataStore.edit { preferences ->
|
||||
preferences[IS_CHECK_IN] = isCheckIn
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun clearLoginData() {
|
||||
dataStore.edit { preferences ->
|
||||
preferences.remove(CHECK_IN_Date)
|
||||
preferences.remove(IS_CHECK_OUT)
|
||||
preferences.remove(IS_CHECK_IN)
|
||||
preferences.remove(CHECK_IN_KEY)
|
||||
preferences.remove(CHECK_OUT_KEY)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.home.ui.componets
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.home.ui.componets
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.PermissionState
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.shouldShowRationale
|
||||
|
||||
|
||||
@Composable
|
||||
fun DefaultScreen(
|
||||
qrCodeValue: String?,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
qrCodeValue?.let {
|
||||
Text(text = "Scanned Value: $it", modifier = Modifier.padding(16.dp))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -8,14 +8,17 @@ 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
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
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.wrapContentSize
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
@@ -26,6 +29,7 @@ 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
|
||||
@@ -40,7 +44,6 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import `in`.sminnovations.attendanceapp.screens.home.ui.componets.DefaultScreen
|
||||
import `in`.sminnovations.attendanceapp.screens.home.ui.componets.LargeButton
|
||||
import `in`.sminnovations.attendanceapp.screens.home.ui.componets.QRCodeScannerScreen
|
||||
import `in`.sminnovations.attendanceapp.screens.home.ui.componets.RequestPermissions
|
||||
@@ -51,19 +54,18 @@ import `in`.sminnovations.attendanceapp.screens.home.viewModel.HomeViewModel
|
||||
fun HomeScreen(viewModel: HomeViewModel = hiltViewModel()) {
|
||||
val isScanning by viewModel.isScanning.collectAsState()
|
||||
val qrCodeValue by viewModel.qrCodeValue.collectAsState()
|
||||
val checkInTime by viewModel.checkedIn.collectAsState()
|
||||
val checkOutTime by viewModel.checkedOut.collectAsState()
|
||||
val userName by viewModel.userName.collectAsState()
|
||||
val isLoading by viewModel.isLoading.collectAsState()
|
||||
val isCheckedIn by viewModel.isCheckedIn.collectAsState()
|
||||
val isCheckedOut by viewModel.isCheckedOut.collectAsState()
|
||||
val isLoading by viewModel.isLoading.collectAsState()
|
||||
val errorMessage by viewModel.errorMessage.collectAsState()
|
||||
|
||||
val cameraPermissionState = rememberPermissionState(Manifest.permission.CAMERA)
|
||||
val locationPermissionState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
|
||||
|
||||
val allPermissionsGranted = remember {
|
||||
derivedStateOf {
|
||||
cameraPermissionState.status.isGranted && locationPermissionState.status.isGranted
|
||||
cameraPermissionState.status.isGranted
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,11 +73,8 @@ fun HomeScreen(viewModel: HomeViewModel = hiltViewModel()) {
|
||||
if (!allPermissionsGranted.value) {
|
||||
RequestPermissions(
|
||||
cameraPermissionState = cameraPermissionState,
|
||||
locationPermissionState = locationPermissionState
|
||||
)
|
||||
} else {
|
||||
DefaultScreen(qrCodeValue = qrCodeValue)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = isScanning,
|
||||
enter = expandVertically(expandFrom = Alignment.Bottom) + fadeIn(),
|
||||
@@ -93,8 +92,80 @@ fun HomeScreen(viewModel: HomeViewModel = hiltViewModel()) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Loading Indicator
|
||||
if (isLoading) {
|
||||
CircularProgressIndicator(
|
||||
@@ -102,42 +173,22 @@ fun HomeScreen(viewModel: HomeViewModel = hiltViewModel()) {
|
||||
)
|
||||
}
|
||||
|
||||
// Display Error Message
|
||||
errorMessage?.let { message ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = message,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier
|
||||
.background(Color(0xFFFFCDD2)) // Light red background for error
|
||||
.padding(8.dp)
|
||||
.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
OutlinedCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(.47f)
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(10.dp),
|
||||
onClick = { }
|
||||
) {
|
||||
|
||||
OutlinedCard(modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(.4f)
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(10.dp), onClick = { }) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(20.dp)
|
||||
.align(Alignment.TopCenter)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
errorMessage?.let { message ->
|
||||
|
||||
errorMessage?.let { message ->
|
||||
OutlinedCard(
|
||||
modifier = Modifier
|
||||
.wrapContentSize()
|
||||
.align(Alignment.TopCenter)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = message,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
@@ -145,67 +196,32 @@ fun HomeScreen(viewModel: HomeViewModel = hiltViewModel()) {
|
||||
modifier = Modifier
|
||||
.background(Color(0xFFFFCDD2)) // Light red background for error
|
||||
.padding(8.dp)
|
||||
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(visible = isCheckedIn && !isCheckedOut) {
|
||||
Text(
|
||||
text = "You already checked in. Scanning again will Check out.",
|
||||
letterSpacing = 2.sp,
|
||||
lineHeight = 40.sp,
|
||||
fontSize = 30.sp,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(visible = !isCheckedIn && !isCheckedOut) {
|
||||
Text(
|
||||
text = "Please Check In",
|
||||
letterSpacing = 2.sp,
|
||||
lineHeight = 40.sp,
|
||||
fontSize = 30.sp,
|
||||
color = Color.Green,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
Column(Modifier.padding(15.dp)) {
|
||||
AnimatedVisibility(visible = isCheckedIn || isCheckedOut) {
|
||||
Text(text = "Check-in: $checkInTime")
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
}
|
||||
AnimatedVisibility(visible = (isCheckedIn && isCheckedOut) || isCheckedOut ) {
|
||||
Text(text = "Check-out: $checkOutTime")
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Column(Modifier.align(Alignment.BottomCenter)) {
|
||||
LargeButton(
|
||||
text = if (isScanning) "Close" else "Scan",
|
||||
onClick = if (isScanning) {
|
||||
{ viewModel.stopScanning() }
|
||||
} else {
|
||||
{ viewModel.startScanning() }
|
||||
},
|
||||
modifier = Modifier.padding(bottom = 24.dp)
|
||||
)
|
||||
if (isScanning) {
|
||||
BackHandler { viewModel.stopScanning() }
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
LargeButton(text = if (isScanning) "Close Scanner" else "Open Scanner",
|
||||
onClick = if (isScanning) {
|
||||
{
|
||||
viewModel.stopScanning()
|
||||
}
|
||||
} else {
|
||||
{
|
||||
viewModel.startScanning()
|
||||
|
||||
}
|
||||
},
|
||||
modifier = Modifier.padding(bottom = 24.dp))
|
||||
}
|
||||
AnimatedVisibility(visible = !isCheckedIn) {
|
||||
LargeButton(
|
||||
text = "Check In",
|
||||
onClick = { viewModel.loginNow() },
|
||||
modifier = Modifier.padding(bottom = 24.dp)
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(visible = isCheckedIn ) {
|
||||
LargeButton(
|
||||
text = "Check Out",
|
||||
onClick = { viewModel.loginNow() },
|
||||
modifier = Modifier.padding(bottom = 24.dp)
|
||||
)
|
||||
LaunchedEffect(qrCodeValue) {
|
||||
viewModel.login()
|
||||
}
|
||||
if (isScanning) {
|
||||
BackHandler { viewModel.stopScanning() }
|
||||
|
||||
@@ -4,25 +4,21 @@ package `in`.sminnovations.attendanceapp.screens.home.viewModel
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.google.firebase.Timestamp
|
||||
import com.google.firebase.firestore.FirebaseFirestore
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import `in`.sminnovations.attendanceapp.screens.home.data.CheckInCheckOutDataStore
|
||||
import `in`.sminnovations.attendanceapp.screens.login.data.LoginDataStore
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import java.text.SimpleDateFormat
|
||||
import java.time.LocalDate
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class HomeViewModel @Inject constructor(
|
||||
private val loginDataStore: LoginDataStore,
|
||||
private val checkInCheckOutDataStore: CheckInCheckOutDataStore,
|
||||
private val firebaseFirestore: FirebaseFirestore,
|
||||
) : ViewModel() {
|
||||
|
||||
@@ -32,11 +28,9 @@ class HomeViewModel @Inject constructor(
|
||||
private val _isScanning = MutableStateFlow(false)
|
||||
val isScanning: StateFlow<Boolean> = _isScanning.asStateFlow()
|
||||
|
||||
private val _checkedIn = MutableStateFlow("")
|
||||
val checkedIn: StateFlow<String> = _checkedIn.asStateFlow()
|
||||
private val _userName = MutableStateFlow<String?>(null)
|
||||
val userName: StateFlow<String?> = _userName.asStateFlow()
|
||||
|
||||
private val _checkedOut = MutableStateFlow("N/A")
|
||||
val checkedOut: StateFlow<String> = _checkedOut.asStateFlow()
|
||||
|
||||
private val _isCheckedIn = MutableStateFlow(false)
|
||||
val isCheckedIn: StateFlow<Boolean> = _isCheckedIn.asStateFlow()
|
||||
@@ -50,6 +44,7 @@ class HomeViewModel @Inject constructor(
|
||||
private val _isLoading = MutableStateFlow(false)
|
||||
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
|
||||
|
||||
|
||||
private fun postErrorMessage(message: String) {
|
||||
viewModelScope.launch {
|
||||
_errorMessage.emit(message)
|
||||
@@ -58,10 +53,17 @@ class HomeViewModel @Inject constructor(
|
||||
|
||||
fun startScanning() {
|
||||
_isScanning.value = true
|
||||
_errorMessage.value = null
|
||||
_isCheckedIn.value = false
|
||||
_isCheckedOut.value = false
|
||||
_userName.value =""
|
||||
}
|
||||
|
||||
fun stopScanning() {
|
||||
_isScanning.value = false
|
||||
_errorMessage.value = null
|
||||
_isCheckedIn.value = false
|
||||
_isCheckedOut.value = false
|
||||
}
|
||||
|
||||
fun setQRCodeValue(value: String) {
|
||||
@@ -79,175 +81,159 @@ class HomeViewModel @Inject constructor(
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
getCheckInChekout()
|
||||
startScanning()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun getCheckInChekout() {
|
||||
viewModelScope.launch {
|
||||
checkInCheckOutDataStore.checkInId.collect { checkIn ->
|
||||
_checkedIn.value = checkIn
|
||||
}
|
||||
}
|
||||
viewModelScope.launch {
|
||||
checkInCheckOutDataStore.checkOutId.collect { checkOut ->
|
||||
_checkedOut.value = checkOut
|
||||
}
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
checkInCheckOutDataStore.checkInDate.collect {date ->
|
||||
if (date == LocalDate.now().toString()) {
|
||||
_isCheckedIn.value = true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
private fun getFormattedDate(): String {
|
||||
val dateFormat = SimpleDateFormat("dd-MM-yyyy", Locale.getDefault())
|
||||
return dateFormat.format(Date())
|
||||
}
|
||||
|
||||
fun loginNow() {
|
||||
private fun putCheckInAttendance(id: String) {
|
||||
viewModelScope.launch {
|
||||
_isLoading.value = true
|
||||
checkInCheckOutDataStore.checkInDate.collect { checkOutDate ->
|
||||
loginDataStore.loginId.collect { _ ->
|
||||
firebaseFirestore.collection("qr_key").whereEqualTo("id", getFormattedDate())
|
||||
.get().addOnSuccessListener { querySnapshot ->
|
||||
_isLoading.value = false
|
||||
if (!querySnapshot.isEmpty) {
|
||||
for (document in querySnapshot) {
|
||||
if (qrCodeValue.value == document.getString("key")) {
|
||||
viewModelScope.launch {
|
||||
if (checkOutDate != LocalDate.now().toString() || checkOutDate == ""
|
||||
) {
|
||||
putCheckInAttendance("")
|
||||
} else {
|
||||
viewModelScope.launch {
|
||||
_isCheckedIn.value = true
|
||||
}
|
||||
val currentDate = SimpleDateFormat("dd-MM-yyyy", Locale.getDefault()).format(Date())
|
||||
val attendanceRef = firebaseFirestore.collection("attendance_$id").document(currentDate)
|
||||
|
||||
putCheckOutAttendance()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
postErrorMessage("Invalid QR code scanned")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
postErrorMessage("No matching QR key found for today's date")
|
||||
}
|
||||
}.addOnFailureListener { exception ->
|
||||
_isLoading.value = false
|
||||
postErrorMessage("Error fetching QR key: ${exception.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
attendanceRef.get().addOnSuccessListener { document ->
|
||||
_isLoading.value = false
|
||||
if (!document.exists()) {
|
||||
val checkInData = hashMapOf(
|
||||
"DayCompleted" to false,
|
||||
"checkIn" to formatTimestamp(Timestamp.now()),
|
||||
"checkOut" to "NA"
|
||||
)
|
||||
|
||||
|
||||
private fun putCheckInAttendance(location: String) {
|
||||
viewModelScope.launch {
|
||||
_isLoading.value = true
|
||||
loginDataStore.loginId.collect { id ->
|
||||
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 checkInData = hashMapOf(
|
||||
"Status" to false,
|
||||
"checkIn" to formatTimestamp(Timestamp.now()),
|
||||
"location" to location,
|
||||
"checkOut" to "NA"
|
||||
)
|
||||
|
||||
attendanceRef.set(checkInData).addOnSuccessListener {
|
||||
viewModelScope.launch {
|
||||
checkInCheckOutDataStore.saveLoginDate(
|
||||
checkDate = LocalDate.now().toString(),
|
||||
checkInTime = formatTimestamp(Timestamp.now())
|
||||
)
|
||||
}
|
||||
Log.d("Firestore", "Check-in successful")
|
||||
viewModelScope.launch {
|
||||
_isCheckedIn.value = true
|
||||
}
|
||||
}.addOnFailureListener { e ->
|
||||
postErrorMessage("Error adding check-in: ${e.message}")
|
||||
}
|
||||
} else {
|
||||
postErrorMessage("Check-in already exists for today. Cannot check in again.")
|
||||
attendanceRef.set(checkInData).addOnSuccessListener {
|
||||
Log.d("Firestore", "Check-in successful")
|
||||
viewModelScope.launch {
|
||||
_isCheckedIn.value = true
|
||||
_isCheckedIn.value =true
|
||||
}
|
||||
}.addOnFailureListener { e ->
|
||||
postErrorMessage("Error adding check-in: ${e.message}")
|
||||
}
|
||||
}.addOnFailureListener { exception ->
|
||||
_isLoading.value = false
|
||||
postErrorMessage("Error fetching document: ${exception.message}")
|
||||
} else {
|
||||
putCheckOutAttendance(id)
|
||||
}
|
||||
}.addOnFailureListener { exception ->
|
||||
_isLoading.value = false
|
||||
postErrorMessage("Error fetching document: ${exception.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun putCheckOutAttendance() {
|
||||
private fun putCheckOutAttendance(id: String) {
|
||||
viewModelScope.launch {
|
||||
_isLoading.value = true
|
||||
loginDataStore.loginId.collect { id ->
|
||||
val currentDate = SimpleDateFormat("dd-MM-yyyy", Locale.getDefault()).format(Date())
|
||||
val attendanceRef =
|
||||
firebaseFirestore.collection("attendance_$id").document(currentDate)
|
||||
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")
|
||||
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()), "Status" to true
|
||||
) as Map<String, Any>
|
||||
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 {
|
||||
checkInCheckOutDataStore.saveLoginData(
|
||||
checkOut = formatTimestamp(
|
||||
Timestamp.now()
|
||||
)
|
||||
)
|
||||
checkInCheckOutDataStore.saveIsLogIn(false)
|
||||
}
|
||||
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.")
|
||||
attendanceRef.update(checkOutData).addOnSuccessListener {
|
||||
viewModelScope.launch {
|
||||
_isCheckedOut.value = true
|
||||
_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.")
|
||||
} 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) {
|
||||
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 {
|
||||
postErrorMessage("Cannot check out without checking in.")
|
||||
_isLoading.value = false
|
||||
postErrorMessage("Error fetching ID }")
|
||||
|
||||
|
||||
}
|
||||
} else {
|
||||
postErrorMessage("No attendance found for today. Please check in first.")
|
||||
}.addOnFailureListener {
|
||||
_isLoading.value = false
|
||||
postErrorMessage("Error FailureListener}")
|
||||
}
|
||||
}.addOnFailureListener { exception ->
|
||||
_isLoading.value = false
|
||||
postErrorMessage("Error fetching document: ${exception.message}")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
viewModelScope.launch {
|
||||
_qrCodeValue.value = ""
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.login.data
|
||||
// File: LoginDataStore.kt
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
private const val DATASTORE_NAME = "user_prefs"
|
||||
private val Context.dataStore by preferencesDataStore(name = DATASTORE_NAME)
|
||||
|
||||
@Singleton
|
||||
class LoginDataStore @Inject constructor(@ApplicationContext context: Context) {
|
||||
|
||||
private val dataStore = context.dataStore
|
||||
|
||||
private val LOGIN_ID_KEY = stringPreferencesKey("login_id")
|
||||
private val PASSWORD_KEY = stringPreferencesKey("password")
|
||||
private val IS_LOGGED_IN_KEY = booleanPreferencesKey("is_logged_in")
|
||||
|
||||
val loginId: Flow<String> = dataStore.data.map { preferences ->
|
||||
preferences[LOGIN_ID_KEY] ?: ""
|
||||
}
|
||||
|
||||
val password: Flow<String> = dataStore.data.map { preferences ->
|
||||
preferences[PASSWORD_KEY] ?: ""
|
||||
}
|
||||
|
||||
val isLoggedIn: Flow<Boolean> = dataStore.data.map { preferences ->
|
||||
preferences[IS_LOGGED_IN_KEY] ?: false
|
||||
}
|
||||
|
||||
suspend fun saveLoginData(id: String, password: String) {
|
||||
dataStore.edit { preferences ->
|
||||
preferences[LOGIN_ID_KEY] = id
|
||||
preferences[PASSWORD_KEY] = password
|
||||
preferences[IS_LOGGED_IN_KEY] = true
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun clearLoginData() {
|
||||
dataStore.edit { preferences ->
|
||||
preferences[LOGIN_ID_KEY] = ""
|
||||
preferences[PASSWORD_KEY] = ""
|
||||
preferences[IS_LOGGED_IN_KEY] = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.login.data
|
||||
|
||||
data class LoginData(
|
||||
val id : String ="",
|
||||
val password: String = ""
|
||||
)
|
||||
@@ -1,55 +0,0 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.login.ui.componets
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
@Composable
|
||||
fun OutlinedTextFieldWithError(
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
label: String,
|
||||
isError: Boolean,
|
||||
errorMessage: String,
|
||||
modifier: Modifier = Modifier,
|
||||
leadingIcon: @Composable (() -> Unit)? = null,
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
visualTransformation: VisualTransformation = VisualTransformation.None
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
OutlinedTextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
label = { Text(label) },
|
||||
isError = isError,
|
||||
leadingIcon = leadingIcon,
|
||||
trailingIcon = trailingIcon,
|
||||
visualTransformation = visualTransformation,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = if (isError) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary,
|
||||
unfocusedBorderColor = if (isError) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
||||
focusedLabelColor = if (isError) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary,
|
||||
unfocusedLabelColor = if (isError) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
)
|
||||
if (isError) {
|
||||
Text(
|
||||
text = errorMessage,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = TextStyle(fontSize = 12.sp),
|
||||
modifier = Modifier.padding(start = 16.dp, top = 4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.login.ui
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
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.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Lock
|
||||
import androidx.compose.material.icons.filled.Person
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import `in`.sminnovations.attendanceapp.R
|
||||
import `in`.sminnovations.attendanceapp.screens.login.ui.componets.OutlinedTextFieldWithError
|
||||
import `in`.sminnovations.attendanceapp.screens.login.viewModel.LoginViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
@Composable
|
||||
fun LoginScreen(
|
||||
loginViewModel: LoginViewModel = hiltViewModel(),
|
||||
onLoginSuccess: () -> Unit
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val isPasswordError by loginViewModel.isPasswordError.collectAsState()
|
||||
val isUserIdError by loginViewModel.isIdError.collectAsState()
|
||||
val isLoggedIn by loginViewModel.isLoggedIn.collectAsState()
|
||||
|
||||
var userId by remember { mutableStateOf("") }
|
||||
var password by remember { mutableStateOf("") }
|
||||
var passwordVisible by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(isLoggedIn) {
|
||||
if (isLoggedIn) onLoginSuccess()
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 16.dp, vertical = 24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(text = "Attendance App", style = MaterialTheme.typography.headlineLarge)
|
||||
Spacer(modifier = Modifier.height(40.dp))
|
||||
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.smi_logo),
|
||||
contentDescription = "App Logo",
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.5f)
|
||||
.aspectRatio(1f)
|
||||
.padding(8.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(40.dp))
|
||||
Text(text = "Credentials", style = MaterialTheme.typography.headlineMedium)
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
OutlinedTextFieldWithError(
|
||||
value = userId,
|
||||
onValueChange = { userId = it },
|
||||
label = "User ID",
|
||||
isError = isUserIdError,
|
||||
errorMessage = "Please enter a valid User ID",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Person,
|
||||
contentDescription = "User Icon"
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
OutlinedTextFieldWithError(
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
label = "Password",
|
||||
isError = isPasswordError,
|
||||
errorMessage = "Please enter a valid password",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Lock,
|
||||
contentDescription = "Password Icon"
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
IconButton(onClick = { passwordVisible = !passwordVisible }) {
|
||||
Icon(
|
||||
painter = painterResource(
|
||||
id = if (passwordVisible) R.drawable.baseline_visibility_24 else R.drawable.baseline_visibility_off_24
|
||||
),
|
||||
contentDescription = if (passwordVisible) "Hide Password" else "Show Password"
|
||||
)
|
||||
}
|
||||
},
|
||||
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation()
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
loginViewModel.login(userId, password, onLoginSuccess)
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(50.dp),
|
||||
) {
|
||||
Text(
|
||||
text = "Login",
|
||||
fontSize = 20.sp
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.login.viewModel
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.google.firebase.firestore.DocumentSnapshot
|
||||
import com.google.firebase.firestore.FirebaseFirestore
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import `in`.sminnovations.attendanceapp.data.dataStore.Employee
|
||||
import `in`.sminnovations.attendanceapp.data.dataStore.EmployeeRepository
|
||||
import `in`.sminnovations.attendanceapp.screens.login.data.LoginData
|
||||
import `in`.sminnovations.attendanceapp.screens.login.data.LoginDataStore
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.tasks.await
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class LoginViewModel @Inject constructor(
|
||||
private val loginDataStore: LoginDataStore,
|
||||
private val firebaseFirestore: FirebaseFirestore,
|
||||
private val employeeRepository: EmployeeRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val _isPasswordError = MutableStateFlow(false)
|
||||
val isPasswordError = _isPasswordError.asStateFlow()
|
||||
|
||||
private val _isIDError = MutableStateFlow(false)
|
||||
val isIdError = _isIDError.asStateFlow()
|
||||
|
||||
private val _isLoggedIn = MutableStateFlow(false)
|
||||
val isLoggedIn = _isLoggedIn.asStateFlow()
|
||||
|
||||
|
||||
private val _loginData = MutableStateFlow(LoginData("", ""))
|
||||
val loginData = _loginData.asStateFlow()
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
collectLoginData()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun collectLoginData(){
|
||||
viewModelScope.launch {
|
||||
loginDataStore.isLoggedIn.collect { loggedIn ->
|
||||
_isLoggedIn.value = loggedIn
|
||||
if (loggedIn) {
|
||||
loginDataStore.loginId.collect { id ->
|
||||
loginDataStore.password.collect { password ->
|
||||
_loginData.value = LoginData(id, password)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun login(userId: String, password: String, onLoginSuccess: () -> Unit) {
|
||||
viewModelScope.launch {
|
||||
firebaseFirestore.collection("employees").whereEqualTo("Id", userId).get()
|
||||
.addOnSuccessListener {
|
||||
if (!it.isEmpty) {
|
||||
_isIDError.value = false
|
||||
for (doc in it) {
|
||||
if (password == doc.getString("Password")) {
|
||||
if (true == doc.getBoolean("EmployeeStatus")) {
|
||||
viewModelScope.launch {
|
||||
loginDataStore.saveLoginData(userId, password)
|
||||
_isLoggedIn.value = true
|
||||
}
|
||||
viewModelScope.launch {
|
||||
fetchEmployeeData(userId)
|
||||
onLoginSuccess()
|
||||
}
|
||||
}else{
|
||||
_isIDError.value = true
|
||||
}
|
||||
} else {
|
||||
_isPasswordError.value = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_isIDError.value = true
|
||||
}
|
||||
}.addOnFailureListener {
|
||||
_isIDError.value = true
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun DocumentSnapshot.toEmployee(): Employee {
|
||||
return Employee(
|
||||
androidID = getString("AndroidID") ?: "",
|
||||
dateOfJoining = getString("DateOfJoining") ?: "",
|
||||
dateOfLeaving = getString("DateOfLeaving") ?: "",
|
||||
designation = getString("Designation") ?: "",
|
||||
employeeStatus = getBoolean("EmployeeStatus") ?: true,
|
||||
id = getString("Id") ?: "",
|
||||
name = getString("Name") ?: "",
|
||||
password = getString("Password") ?: "",
|
||||
photo = getString("Photo") ?: "",
|
||||
dateOfBirth = getString("dateOfBirth") ?: ""
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun fetchEmployeeData(employeeId: String) {
|
||||
val db = firebaseFirestore
|
||||
val document = db.collection("employees").document(employeeId).get().await()
|
||||
|
||||
if (document.exists()) {
|
||||
val savedata = document.toEmployee()
|
||||
Log.d("FetchSuccess", "Fetched Employee Data: $savedata")
|
||||
employeeRepository.saveEmployeeData(savedata)
|
||||
} else {
|
||||
Log.d("FetchError", "Document does not exist for employee ID: $employeeId")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.qrcode.ui
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.settings.data
|
||||
|
||||
class EmployeeDataSettings (
|
||||
val name : String = "",
|
||||
val id : String = "",
|
||||
val designation : String = ""
|
||||
)
|
||||
@@ -1,33 +0,0 @@
|
||||
package `in`.sminnovations.attendanceapp.screens.settings.viewModel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import `in`.sminnovations.attendanceapp.data.dataStore.EmployeeRepository
|
||||
import `in`.sminnovations.attendanceapp.screens.home.data.CheckInCheckOutDataStore
|
||||
import `in`.sminnovations.attendanceapp.screens.login.data.LoginDataStore
|
||||
import `in`.sminnovations.attendanceapp.screens.settings.data.EmployeeDataSettings
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class SettingsViewModel @Inject constructor(
|
||||
private val loginDataStore: LoginDataStore,
|
||||
private val employeeRepository: EmployeeRepository,
|
||||
private val checkInCheckOutDataStore: CheckInCheckOutDataStore,
|
||||
) : ViewModel() {
|
||||
val employeeDataSettings : Flow<EmployeeDataSettings> = employeeRepository.getEmployeeDataSettings()
|
||||
fun logout() {
|
||||
viewModelScope.launch {
|
||||
loginDataStore.clearLoginData()
|
||||
checkInCheckOutDataStore.clearLoginData()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user