Compare commits
40 Commits
dev-user-a
...
dev-sacnne
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bbfd204b7d | ||
|
|
0920d72d2e | ||
|
|
c45af45481 | ||
|
|
8731f27f83 | ||
|
|
ac9728174a | ||
|
|
f1c9af72b7 | ||
|
|
bcd8c77887 | ||
|
|
71dfac73f6 | ||
|
|
cb628a684b | ||
|
|
66832733d9 | ||
|
|
c47f75e9f2 | ||
|
|
3d144f3876 | ||
|
|
5afbcc6670 | ||
|
|
58dd00db11 | ||
|
|
810bbfc327 | ||
|
|
1efbb9af0f | ||
|
|
204a0084da | ||
|
|
fcc831a63d | ||
|
|
ac3a4ebe0f | ||
|
|
ee1f6b8884 | ||
|
|
e435e0ff14 | ||
|
|
de07673a29 | ||
|
|
de4a5c9f8d | ||
|
|
6c322efd93 | ||
|
|
72d1fcdaea | ||
|
|
7d51ac210e | ||
|
|
fc803c6fe2 | ||
|
|
7cc572f201 | ||
|
|
bb59f43dfe | ||
|
|
a48f7ff007 | ||
|
|
c5b52711d0 | ||
|
|
2f70040f54 | ||
|
|
fcbda41419 | ||
|
|
13556dd84c | ||
|
|
7d63df0c83 | ||
|
|
814f9c5c48 | ||
|
|
e8cc833455 | ||
|
|
cdc3a9cf75 | ||
|
|
6b9f54184e | ||
|
|
10b06b2019 |
3
.idea/deploymentTargetSelector.xml
generated
@@ -19,6 +19,9 @@
|
|||||||
<SelectionState runConfigName="checkTime">
|
<SelectionState runConfigName="checkTime">
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
</SelectionState>
|
</SelectionState>
|
||||||
|
<SelectionState runConfigName="AnimatedPreloader">
|
||||||
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
|
</SelectionState>
|
||||||
</selectionStates>
|
</selectionStates>
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
13
.idea/other.xml
generated
@@ -14,6 +14,17 @@
|
|||||||
<option name="screenX" value="720" />
|
<option name="screenX" value="720" />
|
||||||
<option name="screenY" value="1280" />
|
<option name="screenY" value="1280" />
|
||||||
</PersistentDeviceSelectionData>
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="OPPO" />
|
||||||
|
<option name="codename" value="OP573DL1" />
|
||||||
|
<option name="id" value="OP573DL1" />
|
||||||
|
<option name="manufacturer" value="OPPO" />
|
||||||
|
<option name="name" value="CPH2557" />
|
||||||
|
<option name="screenDensity" value="480" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2400" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
<PersistentDeviceSelectionData>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="28" />
|
<option name="api" value="28" />
|
||||||
<option name="brand" value="DOCOMO" />
|
<option name="brand" value="DOCOMO" />
|
||||||
@@ -262,7 +273,7 @@
|
|||||||
<option name="codename" value="q6q" />
|
<option name="codename" value="q6q" />
|
||||||
<option name="id" value="q6q" />
|
<option name="id" value="q6q" />
|
||||||
<option name="manufacturer" value="Samsung" />
|
<option name="manufacturer" value="Samsung" />
|
||||||
<option name="name" value="SM-F956B" />
|
<option name="name" value="Galaxy Z Fold6" />
|
||||||
<option name="screenDensity" value="420" />
|
<option name="screenDensity" value="420" />
|
||||||
<option name="screenX" value="1856" />
|
<option name="screenX" value="1856" />
|
||||||
<option name="screenY" value="2160" />
|
<option name="screenY" value="2160" />
|
||||||
|
|||||||
2
.idea/vcs.xml
generated
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="VcsDirectoryMappings">
|
<component name="VcsDirectoryMappings">
|
||||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
<mapping directory="" vcs="Git" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -15,8 +15,9 @@ android {
|
|||||||
applicationId = "in.sminnovations.attendanceapp.user"
|
applicationId = "in.sminnovations.attendanceapp.user"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 1
|
versionCode = 4
|
||||||
versionName = "1.0"
|
|
||||||
|
versionName = "3.0.4"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
@@ -31,6 +32,7 @@ android {
|
|||||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
"proguard-rules.pro"
|
"proguard-rules.pro"
|
||||||
)
|
)
|
||||||
|
signingConfig = signingConfigs.getByName("debug")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
@@ -55,6 +57,7 @@ android {
|
|||||||
kapt {
|
kapt {
|
||||||
correctErrorTypes = true
|
correctErrorTypes = true
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
@@ -103,4 +106,7 @@ dependencies {
|
|||||||
implementation("com.google.firebase:firebase-analytics")
|
implementation("com.google.firebase:firebase-analytics")
|
||||||
implementation ("com.google.firebase:firebase-firestore-ktx:25.1.0") // Use the latest version
|
implementation ("com.google.firebase:firebase-firestore-ktx:25.1.0") // Use the latest version
|
||||||
implementation("com.google.firebase:firebase-firestore")
|
implementation("com.google.firebase:firebase-firestore")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -4,11 +4,10 @@
|
|||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.CAMERA"/>
|
<uses-permission android:name="android.permission.CAMERA"/>
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
|
||||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||||
<application
|
<application
|
||||||
android:name=".MyApp"
|
android:name=".main.MyApp"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
@@ -19,12 +18,14 @@
|
|||||||
android:theme="@style/Theme.SMIAttendance"
|
android:theme="@style/Theme.SMIAttendance"
|
||||||
tools:targetApi="31">
|
tools:targetApi="31">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".main.MainAppActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:theme="@style/Theme.SMIAttendance">
|
android:theme="@style/Theme.SMIAttendance">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
<category android:name="android.intent.category.HOME" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
</application>
|
</application>
|
||||||
|
|||||||
BIN
app/src/main/ic_launcher-playstore.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
@@ -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,22 +0,0 @@
|
|||||||
package `in`.sminnovations.attendanceapp
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.activity.ComponentActivity
|
|
||||||
import androidx.activity.compose.setContent
|
|
||||||
import androidx.activity.enableEdgeToEdge
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import `in`.sminnovations.attendanceapp.ui.theme.SMIAttendanceTheme
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
class MainActivity : ComponentActivity() {
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
enableEdgeToEdge()
|
|
||||||
setContent {
|
|
||||||
SMIAttendanceTheme {
|
|
||||||
AppNavigation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -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,42 +1,25 @@
|
|||||||
package `in`.sminnovations.attendanceapp.di
|
package `in`.sminnovations.attendanceapp.di
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.datastore.core.DataStore
|
|
||||||
import androidx.datastore.preferences.core.Preferences
|
|
||||||
import androidx.datastore.preferences.preferencesDataStore
|
|
||||||
import com.google.firebase.firestore.FirebaseFirestore
|
import com.google.firebase.firestore.FirebaseFirestore
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import `in`.sminnovations.attendanceapp.screens.home.data.CheckInCheckOutDataStore
|
import `in`.sminnovations.attendanceapp.main.NetworkMonitor
|
||||||
import `in`.sminnovations.attendanceapp.screens.login.data.LoginDataStore
|
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
object AppModule {
|
object AppModule {
|
||||||
|
|
||||||
private val Context.dataStore by preferencesDataStore(name = "employee_data")
|
|
||||||
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideAppContext(@ApplicationContext context: Context): Context {
|
fun provideAppContext(@ApplicationContext context: Context): Context {
|
||||||
return context
|
return context
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun provideLoginDataStore(@ApplicationContext context: Context): LoginDataStore {
|
|
||||||
return LoginDataStore(context)
|
|
||||||
}
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun provideCheckInCheckOutDataStore(@ApplicationContext context: Context): CheckInCheckOutDataStore {
|
|
||||||
return CheckInCheckOutDataStore(context)
|
|
||||||
}
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideFirebase(): FirebaseFirestore {
|
fun provideFirebase(): FirebaseFirestore {
|
||||||
@@ -45,13 +28,7 @@ object AppModule {
|
|||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideEmployeeDataStore(@ApplicationContext context: Context): DataStore<Preferences> {
|
fun provideNetworkMonitor(@ApplicationContext context: Context): NetworkMonitor {
|
||||||
return context.dataStore
|
return NetworkMonitor(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
// fun provideFirebase(): FirebaseFirestore {
|
|
||||||
// return FirebaseFirestore.getInstance()
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
package `in`.sminnovations.attendanceapp
|
package `in`.sminnovations.attendanceapp.main
|
||||||
|
|
||||||
// File: MyApp.kt
|
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package `in`.sminnovations.attendanceapp.main
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.os.BatteryManager
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.WindowManager
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import `in`.sminnovations.attendanceapp.ui.theme.SMIAttendanceTheme
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class MainAppActivity : ComponentActivity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
if (shouldKeepScreenOn()) {
|
||||||
|
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||||
|
} else {
|
||||||
|
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||||
|
}
|
||||||
|
enableEdgeToEdge()
|
||||||
|
setContent {
|
||||||
|
SMIAttendanceTheme {
|
||||||
|
MainScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun shouldKeepScreenOn(): Boolean {
|
||||||
|
val batteryStatus = registerReceiver(
|
||||||
|
null,
|
||||||
|
IntentFilter(Intent.ACTION_BATTERY_CHANGED)
|
||||||
|
) ?: return false
|
||||||
|
val level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
|
||||||
|
val scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
|
||||||
|
val isCharging = batteryStatus.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) > 0
|
||||||
|
val batteryPct = level * 100 / scale
|
||||||
|
return batteryPct > 50 || isCharging
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
package `in`.sminnovations.attendanceapp
|
package `in`.sminnovations.attendanceapp.main
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.navigation.NavController
|
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import `in`.sminnovations.attendanceapp.navigation.BottomNavigationBar
|
import `in`.sminnovations.attendanceapp.navigation.BottomNavigationBar
|
||||||
import `in`.sminnovations.attendanceapp.navigation.Navigation
|
import `in`.sminnovations.attendanceapp.navigation.Navigation
|
||||||
@@ -14,7 +13,6 @@ import `in`.sminnovations.attendanceapp.navigation.data.navItems
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MainScreen(
|
fun MainScreen(
|
||||||
mainNavController: NavController
|
|
||||||
) {
|
) {
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
Scaffold(
|
Scaffold(
|
||||||
@@ -23,7 +21,7 @@ fun MainScreen(
|
|||||||
},
|
},
|
||||||
content = { padding ->
|
content = { padding ->
|
||||||
Box(modifier = Modifier.padding(padding)) {
|
Box(modifier = Modifier.padding(padding)) {
|
||||||
Navigation(navController = navController, mainNavHostController = mainNavController, items = navItems)
|
Navigation(navController = navController, items = navItems)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package `in`.sminnovations.attendanceapp.main
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.Network
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
|
import android.net.NetworkRequest
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class NetworkMonitor @Inject constructor(
|
||||||
|
@ApplicationContext private val context: Context
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val connectivityManager =
|
||||||
|
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||||
|
|
||||||
|
private val _isConnected = MutableLiveData(false)
|
||||||
|
val isConnected: LiveData<Boolean> get() = _isConnected
|
||||||
|
|
||||||
|
private val networkScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
|
||||||
|
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
|
||||||
|
override fun onAvailable(network: Network) {
|
||||||
|
performPingCheck()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLost(network: Network) {
|
||||||
|
_isConnected.postValue(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startMonitoring() {
|
||||||
|
val networkRequest = NetworkRequest.Builder()
|
||||||
|
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
connectivityManager.registerNetworkCallback(networkRequest, networkCallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopMonitoring() {
|
||||||
|
try {
|
||||||
|
connectivityManager.unregisterNetworkCallback(networkCallback)
|
||||||
|
} catch (_: IllegalArgumentException) {
|
||||||
|
}
|
||||||
|
networkScope.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun performPingCheck() {
|
||||||
|
networkScope.launch {
|
||||||
|
try {
|
||||||
|
// Ping Google's public DNS server
|
||||||
|
val process = Runtime.getRuntime().exec("/system/bin/ping -c 1 8.8.8.8")
|
||||||
|
val isReachable = process.waitFor() == 0
|
||||||
|
_isConnected.postValue(isReachable)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_isConnected.postValue(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,14 +6,13 @@ import androidx.navigation.NavHostController
|
|||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import `in`.sminnovations.attendanceapp.navigation.data.BottomNavItem
|
import `in`.sminnovations.attendanceapp.navigation.data.BottomNavItem
|
||||||
import `in`.sminnovations.attendanceapp.screens.history.ui.HistoryScreen
|
|
||||||
import `in`.sminnovations.attendanceapp.screens.home.ui.HomeScreen
|
import `in`.sminnovations.attendanceapp.screens.home.ui.HomeScreen
|
||||||
import `in`.sminnovations.attendanceapp.screens.settings.ui.SettingsScreen
|
import `in`.sminnovations.attendanceapp.screens.settings.ui.SettingsScreen
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Navigation(
|
fun Navigation(
|
||||||
mainNavHostController: NavController,
|
|
||||||
navController: NavHostController,
|
navController: NavHostController,
|
||||||
items: List<BottomNavItem>,
|
items: List<BottomNavItem>,
|
||||||
) {
|
) {
|
||||||
@@ -22,10 +21,7 @@ fun Navigation(
|
|||||||
HomeScreen()
|
HomeScreen()
|
||||||
}
|
}
|
||||||
composable(items[1].route) {
|
composable(items[1].route) {
|
||||||
HistoryScreen()
|
SettingsScreen()
|
||||||
}
|
|
||||||
composable(items[2].route) {
|
|
||||||
SettingsScreen(mainNavHostController)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,5 @@ data class BottomNavItem(
|
|||||||
)
|
)
|
||||||
val navItems = listOf(
|
val navItems = listOf(
|
||||||
BottomNavItem(Icons.Outlined.Search, Icons.Filled.Search, "Scan", "scan"),
|
BottomNavItem(Icons.Outlined.Search, Icons.Filled.Search, "Scan", "scan"),
|
||||||
BottomNavItem(Icons.Outlined.AddCircle, Icons.Filled.AddCircle, "Records", "records"),
|
|
||||||
BottomNavItem(Icons.Outlined.Settings, Icons.Filled.Settings, "Settings", "settings")
|
BottomNavItem(Icons.Outlined.Settings, Icons.Filled.Settings, "Settings", "settings")
|
||||||
)
|
)
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
package `in`.sminnovations.attendanceapp.screens.home.ui.componets
|
||||||
|
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
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.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.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.CardDefaults
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedCard
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
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 com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
|
import com.google.accompanist.permissions.PermissionState
|
||||||
|
import com.google.accompanist.permissions.isGranted
|
||||||
|
|
||||||
|
import `in`.sminnovations.attendanceapp.screens.home.viewModel.AttendanceUiState
|
||||||
|
|
||||||
|
@OptIn(ExperimentalPermissionsApi::class)
|
||||||
|
@Composable
|
||||||
|
internal 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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uiState.isLoading) {
|
||||||
|
LoadingOverlay()
|
||||||
|
}
|
||||||
|
|
||||||
|
ScannerToggleButton(
|
||||||
|
isScanning = uiState.isScanning,
|
||||||
|
onClick = onScannerToggle,
|
||||||
|
modifier = Modifier.align(Alignment.BottomCenter)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
Text(
|
||||||
|
text = userName ?: "",
|
||||||
|
letterSpacing = 2.sp,
|
||||||
|
lineHeight = 40.sp,
|
||||||
|
fontSize = 30.sp,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BackHandler { onClose() }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
package `in`.sminnovations.attendanceapp.screens.home.ui.componets
|
|
||||||
|
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package `in`.sminnovations.attendanceapp.screens.home.ui.componets
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NetworkErrorDialog(
|
||||||
|
showDialog: Boolean,
|
||||||
|
onRetry: () -> Unit
|
||||||
|
) {
|
||||||
|
if (showDialog) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { /* Do nothing to prevent dismissal by tapping outside */ },
|
||||||
|
title = {
|
||||||
|
Text(text = "Network Error")
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Text(text = "Network is not connected. Please check your Wi-Fi or mobile data.\n\nIf Wi-Fi is just connected wait for some time and press retry again")
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
onRetry()
|
||||||
|
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text("Retry")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.padding(16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
package `in`.sminnovations.attendanceapp.screens.home.ui.componets
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedCard
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
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.withStyle
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun ErrorContent(
|
||||||
|
errorMessage: String,
|
||||||
|
userName: String?,
|
||||||
|
checkedOutTime: String
|
||||||
|
) {
|
||||||
|
|
||||||
|
val errorExplanations = mapOf(
|
||||||
|
// Network and Data Access Errors
|
||||||
|
"Failed to fetch employee data" to "There was an issue retrieving your employee information. Please check your internet connection and try again.",
|
||||||
|
"Failed to fetch attendance data" to "Unable to retrieve attendance records. Please verify your internet connection and try again.",
|
||||||
|
|
||||||
|
// Employee Validation Errors
|
||||||
|
"Invalid employee ID" to "The scanned ID isn't recognized. Double-check the ID, and if the issue continues, contact your administrator.",
|
||||||
|
"Employee ID has expired" to "This ID is no longer active. Please contact HR to renew or reactivate your ID.",
|
||||||
|
|
||||||
|
// Attendance Process Errors
|
||||||
|
"Invalid attendance record" to "An unexpected issue occurred with your attendance record. Please contact support for assistance.",
|
||||||
|
"Already checked out for today." to "You've already completed your check-out for today. If you need to make corrections, please contact support.",
|
||||||
|
"Failed to check in" to "There was an issue recording your check-in. Please try again or contact support if the problem persists.",
|
||||||
|
"Failed to check out" to "There was an issue recording your check-out. Please try again or contact support if the problem persists.",
|
||||||
|
|
||||||
|
// Generic Error
|
||||||
|
"An error occurred" to "An unexpected error occurred. Please try again or contact support if the issue continues."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
val detailedExplanation = errorExplanations[errorMessage] ?: "An unexpected error occurred."
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OutlinedCard(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 16.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = errorMessage,
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
modifier = Modifier.padding(16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detailed explanation for the error
|
||||||
|
OutlinedCard(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = if (checkedOutTime.isNotEmpty()) 16.dp else 0.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
letterSpacing = 2.sp,
|
||||||
|
lineHeight = 40.sp,
|
||||||
|
text = detailedExplanation,
|
||||||
|
fontSize = 18.sp,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
modifier = Modifier.padding(16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
package `in`.sminnovations.attendanceapp.screens.home.ui.componets
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material3.Button
|
|
||||||
import androidx.compose.material3.ButtonDefaults
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun LargeButton(
|
|
||||||
text: String,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
Button(
|
|
||||||
onClick = onClick,
|
|
||||||
modifier = modifier
|
|
||||||
.height(56.dp)
|
|
||||||
.width(200.dp),
|
|
||||||
colors = ButtonDefaults.buttonColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.primary,
|
|
||||||
contentColor = MaterialTheme.colorScheme.onPrimary
|
|
||||||
),
|
|
||||||
shape = RoundedCornerShape(28.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = text,
|
|
||||||
fontSize = 18.sp,
|
|
||||||
fontWeight = FontWeight.Bold
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -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))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -10,7 +10,6 @@ import androidx.compose.material3.OutlinedCard
|
|||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
@@ -22,7 +21,6 @@ import com.google.mlkit.vision.common.InputImage
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun QRCodeScannerScreen(
|
fun QRCodeScannerScreen(
|
||||||
|
|
||||||
onQRCodeScanned: (String) -> Unit,
|
onQRCodeScanned: (String) -> Unit,
|
||||||
lifecycleOwner: LifecycleOwner = androidx.lifecycle.compose.LocalLifecycleOwner.current
|
lifecycleOwner: LifecycleOwner = androidx.lifecycle.compose.LocalLifecycleOwner.current
|
||||||
) {
|
) {
|
||||||
@@ -61,7 +59,7 @@ fun QRCodeScannerScreen(
|
|||||||
cameraProvider.unbindAll()
|
cameraProvider.unbindAll()
|
||||||
cameraProvider.bindToLifecycle(
|
cameraProvider.bindToLifecycle(
|
||||||
lifecycleOwner,
|
lifecycleOwner,
|
||||||
CameraSelector.DEFAULT_BACK_CAMERA,
|
CameraSelector.DEFAULT_FRONT_CAMERA,
|
||||||
preview,
|
preview,
|
||||||
imageAnalyzer
|
imageAnalyzer
|
||||||
)
|
)
|
||||||
@@ -72,8 +70,9 @@ fun QRCodeScannerScreen(
|
|||||||
|
|
||||||
AndroidView(
|
AndroidView(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.fillMaxHeight(.84f)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.aspectRatio(1f),
|
,
|
||||||
factory = { previewView }
|
factory = { previewView }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
package `in`.sminnovations.attendanceapp.screens.home.ui.componets
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
|
||||||
import com.google.accompanist.permissions.PermissionState
|
|
||||||
import com.google.accompanist.permissions.isGranted
|
|
||||||
|
|
||||||
@OptIn(ExperimentalPermissionsApi::class)
|
|
||||||
@Composable
|
|
||||||
fun RequestPermissions(
|
|
||||||
cameraPermissionState: PermissionState,
|
|
||||||
locationPermissionState: PermissionState
|
|
||||||
) {
|
|
||||||
LaunchedEffect(cameraPermissionState.status.isGranted, locationPermissionState.status.isGranted) {
|
|
||||||
when {
|
|
||||||
!cameraPermissionState.status.isGranted -> {
|
|
||||||
cameraPermissionState.launchPermissionRequest()
|
|
||||||
}
|
|
||||||
!locationPermissionState.status.isGranted -> {
|
|
||||||
locationPermissionState.launchPermissionRequest()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package `in`.sminnovations.attendanceapp.screens.home.ui.componets
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
|
import com.google.accompanist.permissions.PermissionState
|
||||||
|
|
||||||
|
|
||||||
|
@OptIn(ExperimentalPermissionsApi::class)
|
||||||
|
@Composable
|
||||||
|
internal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,217 +1,39 @@
|
|||||||
package `in`.sminnovations.attendanceapp.screens.home.ui
|
package `in`.sminnovations.attendanceapp.screens.home.ui
|
||||||
|
|
||||||
import android.Manifest
|
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.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
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.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.ArrowBack
|
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.OutlinedCard
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
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.Modifier
|
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 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.isGranted
|
|
||||||
import com.google.accompanist.permissions.rememberPermissionState
|
import com.google.accompanist.permissions.rememberPermissionState
|
||||||
import `in`.sminnovations.attendanceapp.screens.home.ui.componets.DefaultScreen
|
import `in`.sminnovations.attendanceapp.screens.home.ui.componets.AttendanceScreenContent
|
||||||
import `in`.sminnovations.attendanceapp.screens.home.ui.componets.LargeButton
|
import `in`.sminnovations.attendanceapp.screens.home.ui.componets.NetworkErrorDialog
|
||||||
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.HomeViewModel
|
import `in`.sminnovations.attendanceapp.screens.home.viewModel.HomeViewModel
|
||||||
|
|
||||||
@OptIn(ExperimentalPermissionsApi::class, ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalPermissionsApi::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 checkInTime by viewModel.checkedIn.collectAsState()
|
) {
|
||||||
val checkOutTime by viewModel.checkedOut.collectAsState()
|
val uiState by viewModel.uiState.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 cameraPermissionState = rememberPermissionState(Manifest.permission.CAMERA)
|
||||||
val locationPermissionState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
|
NetworkErrorDialog(
|
||||||
|
showDialog = !uiState.isConnected,
|
||||||
val allPermissionsGranted = remember {
|
onRetry = {
|
||||||
derivedStateOf {
|
viewModel.reStartMonitoring()
|
||||||
cameraPermissionState.status.isGranted && locationPermissionState.status.isGranted
|
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
|
if (uiState.isConnected){
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
AttendanceScreenContent(
|
||||||
if (!allPermissionsGranted.value) {
|
uiState = uiState,
|
||||||
RequestPermissions(
|
cameraPermissionState = cameraPermissionState,
|
||||||
cameraPermissionState = cameraPermissionState,
|
onScannerToggle = { if (uiState.isScanning) viewModel.stopScanning() else viewModel.startScanning() },
|
||||||
locationPermissionState = locationPermissionState
|
onQrCodeScanned = viewModel::setQRCodeValue,
|
||||||
)
|
modifier = modifier
|
||||||
} else {
|
)
|
||||||
DefaultScreen(qrCodeValue = qrCodeValue)
|
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loading Indicator
|
|
||||||
if (isLoading) {
|
|
||||||
CircularProgressIndicator(
|
|
||||||
modifier = Modifier.align(Alignment.Center)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 = { }
|
|
||||||
) {
|
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(20.dp)
|
|
||||||
.align(Alignment.TopCenter)
|
|
||||||
.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
errorMessage?.let { message ->
|
|
||||||
Text(
|
|
||||||
text = message,
|
|
||||||
color = MaterialTheme.colorScheme.error,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
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() }
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (isScanning) {
|
|
||||||
BackHandler { viewModel.stopScanning() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,256 +1,286 @@
|
|||||||
package `in`.sminnovations.attendanceapp.screens.home.viewModel
|
package `in`.sminnovations.attendanceapp.screens.home.viewModel
|
||||||
|
|
||||||
|
|
||||||
import android.util.Log
|
import android.content.Context
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.asFlow
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
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 `in`.sminnovations.attendanceapp.screens.home.data.CheckInCheckOutDataStore
|
import `in`.sminnovations.attendanceapp.main.NetworkMonitor
|
||||||
import `in`.sminnovations.attendanceapp.screens.login.data.LoginDataStore
|
|
||||||
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.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
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.time.LocalDate
|
|
||||||
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 loginDataStore: LoginDataStore,
|
private val applicationContext: Context,
|
||||||
private val checkInCheckOutDataStore: CheckInCheckOutDataStore,
|
|
||||||
private val firebaseFirestore: FirebaseFirestore,
|
private val firebaseFirestore: FirebaseFirestore,
|
||||||
|
private val networkMonitor: NetworkMonitor
|
||||||
) : 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)
|
init {
|
||||||
val isScanning: StateFlow<Boolean> = _isScanning.asStateFlow()
|
|
||||||
|
|
||||||
private val _checkedIn = MutableStateFlow("")
|
networkMonitor.startMonitoring()
|
||||||
val checkedIn: StateFlow<String> = _checkedIn.asStateFlow()
|
networkMonitor.isConnected.asFlow()
|
||||||
|
.onEach { isConnected ->
|
||||||
private val _checkedOut = MutableStateFlow("N/A")
|
_uiState.update { currentState ->
|
||||||
val checkedOut: StateFlow<String> = _checkedOut.asStateFlow()
|
currentState.copy(isConnected = isConnected)
|
||||||
|
}
|
||||||
private val _isCheckedIn = MutableStateFlow(false)
|
}
|
||||||
val isCheckedIn: StateFlow<Boolean> = _isCheckedIn.asStateFlow()
|
.launchIn(viewModelScope)
|
||||||
|
if (uiState.value.isConnected) {
|
||||||
private val _isCheckedOut = MutableStateFlow(false)
|
viewModelScope.launch {
|
||||||
val isCheckedOut: StateFlow<Boolean> = _isCheckedOut.asStateFlow()
|
startScanning()
|
||||||
|
}
|
||||||
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 stopMonitoring() {
|
||||||
|
networkMonitor.stopMonitoring()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reStartMonitoring() {
|
||||||
|
networkMonitor.stopMonitoring()
|
||||||
|
networkMonitor.startMonitoring()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startScanning() {
|
fun startScanning() {
|
||||||
_isScanning.value = true
|
viewModelScope.launch {
|
||||||
|
_uiState.update { currentState ->
|
||||||
|
AttendanceUiState( // Reset to completely fresh state
|
||||||
|
isScanning = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun stopScanning() {
|
fun stopScanning() {
|
||||||
_isScanning.value = false
|
viewModelScope.launch {
|
||||||
|
_uiState.update { currentState ->
|
||||||
|
currentState.copy(
|
||||||
|
isScanning = false, errorMessage = null, qrCodeValue = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setQRCodeValue(value: String) {
|
fun setQRCodeValue(value: String) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_qrCodeValue.emit(value)
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
qrCodeValue = value,
|
||||||
|
userName = null, // Reset userName when new QR code is scanned
|
||||||
|
errorMessage = null
|
||||||
|
)
|
||||||
|
}
|
||||||
stopScanning()
|
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(
|
||||||
|
"DayCompleted" to false,
|
||||||
|
"checkIn" to formatTimestamp(Timestamp.now()),
|
||||||
|
"checkOut" to "NA"
|
||||||
|
)
|
||||||
|
|
||||||
|
firebaseFirestore.collection("attendance_$userId").document(currentDate)
|
||||||
|
.set(checkInData).await()
|
||||||
|
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
isCheckedIn = true, qrCodeValue = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
handleError("Failed to check in: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun processCheckOut(userId: String, document: DocumentSnapshot) {
|
||||||
|
try {
|
||||||
|
val checkOutData = hashMapOf(
|
||||||
|
"checkOut" to formatTimestamp(Timestamp.now()), "DayCompleted" to true
|
||||||
|
)
|
||||||
|
|
||||||
|
document.reference.update(checkOutData as Map<String, Any>).await()
|
||||||
|
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
isCheckedOut = true, qrCodeValue = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
handleError("Failed to check out: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getEmployeeDocument(userId: String): DocumentSnapshot? {
|
||||||
|
return try {
|
||||||
|
val querySnapshot =
|
||||||
|
firebaseFirestore.collection("employees").whereEqualTo("Id", userId).get().await()
|
||||||
|
|
||||||
|
if (querySnapshot.isEmpty) null else querySnapshot.documents.first()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw Exception("Failed to fetch employee data")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getAttendanceDocument(userId: String, date: String): DocumentSnapshot? {
|
||||||
|
return try {
|
||||||
|
firebaseFirestore.collection("attendance_$userId").document(date).get().await()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw Exception("Failed to fetch attendance data")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun setLoading(loading: Boolean) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_uiState.update { it.copy(isLoading = loading) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCurrentFormattedDate(): String {
|
||||||
|
return SimpleDateFormat("dd-MM-yyyy", Locale.getDefault()).format(Date())
|
||||||
|
}
|
||||||
|
|
||||||
private fun formatTimestamp(timestamp: Timestamp): String {
|
private fun formatTimestamp(timestamp: Timestamp): String {
|
||||||
val sdf =
|
return SimpleDateFormat(
|
||||||
SimpleDateFormat("dd MMMM yyyy hh:mm:ss a", Locale.getDefault()) // Format with AM/PM
|
"dd MMMM yyyy hh:mm:ss a",
|
||||||
return sdf.format(timestamp.toDate())
|
Locale.getDefault()
|
||||||
}
|
).format(timestamp.toDate())
|
||||||
|
|
||||||
init {
|
|
||||||
viewModelScope.launch {
|
|
||||||
getCheckInChekout()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
fun isDeviceConnectedToInternet(context: Context): Boolean {
|
||||||
val dateFormat = SimpleDateFormat("dd-MM-yyyy", Locale.getDefault())
|
val connectivityManager =
|
||||||
return dateFormat.format(Date())
|
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||||
}
|
|
||||||
|
|
||||||
fun loginNow() {
|
val network = connectivityManager.activeNetwork ?: return false
|
||||||
viewModelScope.launch {
|
val networkCapabilities =
|
||||||
_isLoading.value = true
|
connectivityManager.getNetworkCapabilities(network) ?: return false
|
||||||
checkInCheckOutDataStore.checkInDate.collect { checkOutDate ->
|
return when {
|
||||||
loginDataStore.loginId.collect { _ ->
|
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
|
||||||
firebaseFirestore.collection("qr_key").whereEqualTo("id", getFormattedDate())
|
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
|
||||||
.get().addOnSuccessListener { querySnapshot ->
|
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
|
||||||
_isLoading.value = false
|
else -> 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
|
|
||||||
}
|
|
||||||
|
|
||||||
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}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
override fun onCleared() {
|
||||||
|
super.onCleared()
|
||||||
private fun putCheckInAttendance(location: String) {
|
networkMonitor.stopMonitoring()
|
||||||
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.")
|
|
||||||
viewModelScope.launch {
|
|
||||||
_isCheckedIn.value = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.addOnFailureListener { exception ->
|
|
||||||
_isLoading.value = false
|
|
||||||
postErrorMessage("Error fetching document: ${exception.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun putCheckOutAttendance() {
|
|
||||||
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 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>
|
|
||||||
|
|
||||||
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.")
|
|
||||||
viewModelScope.launch {
|
|
||||||
_isCheckedOut.value = true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
postErrorMessage("Cannot check out without checking in.")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
postErrorMessage("No attendance found for today. Please check in first.")
|
|
||||||
}
|
|
||||||
}.addOnFailureListener { exception ->
|
|
||||||
_isLoading.value = false
|
|
||||||
postErrorMessage("Error fetching document: ${exception.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
val isConnected : Boolean =true
|
||||||
|
)
|
||||||
@@ -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 = ""
|
|
||||||
)
|
|
||||||
@@ -11,41 +11,34 @@ import androidx.compose.foundation.rememberScrollState
|
|||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.OutlinedCard
|
import androidx.compose.material3.OutlinedCard
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavController
|
|
||||||
import `in`.sminnovations.attendanceapp.screens.settings.data.SettingsData
|
|
||||||
import `in`.sminnovations.attendanceapp.screens.settings.ui.components.TopComponents
|
import `in`.sminnovations.attendanceapp.screens.settings.ui.components.TopComponents
|
||||||
import `in`.sminnovations.attendanceapp.screens.settings.ui.components.subSetting.AboutSettings
|
import `in`.sminnovations.attendanceapp.screens.settings.ui.components.subSetting.AboutSettings
|
||||||
import `in`.sminnovations.attendanceapp.screens.settings.ui.components.subSetting.Appearance
|
import `in`.sminnovations.attendanceapp.screens.settings.ui.components.subSetting.Developer_settings
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsScreen(
|
fun SettingsScreen(
|
||||||
navController: NavController
|
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val dataStore = SettingsData(context)
|
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
val devMode = dataStore.getDevModeToggle.collectAsState(initial = false)
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.verticalScroll(scrollState)
|
.verticalScroll(scrollState)
|
||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.height(20.dp))
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
TopComponents(navController = navController)
|
TopComponents()
|
||||||
Spacer(modifier = Modifier.height(20.dp))
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
OutlinedCard(
|
OutlinedCard(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(10.dp)
|
.padding(10.dp)
|
||||||
.wrapContentSize()
|
.wrapContentSize()
|
||||||
) {
|
) {
|
||||||
|
|
||||||
Appearance(context = context)
|
|
||||||
AboutSettings(context = context)
|
AboutSettings(context = context)
|
||||||
|
Developer_settings(context = context)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,37 +7,19 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Button
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
|
||||||
import androidx.navigation.NavController
|
|
||||||
import androidx.navigation.compose.rememberNavController
|
|
||||||
import `in`.sminnovations.attendanceapp.R
|
import `in`.sminnovations.attendanceapp.R
|
||||||
import `in`.sminnovations.attendanceapp.screens.settings.data.EmployeeDataSettings
|
|
||||||
import `in`.sminnovations.attendanceapp.screens.settings.viewModel.SettingsViewModel
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TopComponents(
|
fun TopComponents(
|
||||||
navController: NavController,
|
|
||||||
settingsViewModel: SettingsViewModel = hiltViewModel()
|
|
||||||
) {
|
) {
|
||||||
val employeeDataSettings by settingsViewModel.employeeDataSettings.collectAsState(
|
|
||||||
initial = EmployeeDataSettings()
|
|
||||||
)
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally
|
modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
@@ -51,38 +33,5 @@ fun TopComponents(
|
|||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
Text(
|
|
||||||
text = employeeDataSettings.name,
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
fontSize = 20.sp,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = "ID: ${employeeDataSettings.id}",
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
fontSize = 16.sp
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = employeeDataSettings.designation,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.primary,
|
|
||||||
fontSize = 16.sp
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
Button(onClick = {
|
|
||||||
settingsViewModel.logout()
|
|
||||||
navController.navigate("login")
|
|
||||||
}) {
|
|
||||||
Text(text = "Log out")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
package `in`.sminnovations.attendanceapp.screens.settings.ui.components.settingsComponents
|
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
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.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.wrapContentSize
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.OutlinedCard
|
|
||||||
import androidx.compose.material3.RadioButton
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.window.Dialog
|
|
||||||
import androidx.compose.ui.window.DialogProperties
|
|
||||||
import `in`.sminnovations.attendanceapp.R
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun RadioDialogue(
|
|
||||||
showDialog:Boolean,
|
|
||||||
listOptions:List<String>,
|
|
||||||
headText:String,
|
|
||||||
selectedOption:String,
|
|
||||||
onClose: () -> Unit,
|
|
||||||
gotOption:(String)->Unit
|
|
||||||
) {
|
|
||||||
|
|
||||||
if (showDialog) {
|
|
||||||
Dialog(
|
|
||||||
onDismissRequest = { onClose() },
|
|
||||||
DialogProperties(
|
|
||||||
dismissOnBackPress = true,
|
|
||||||
dismissOnClickOutside = true,
|
|
||||||
usePlatformDefaultWidth = true
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
OutlinedCard(
|
|
||||||
modifier = Modifier
|
|
||||||
.wrapContentSize()
|
|
||||||
.clickable { onClose() },
|
|
||||||
) {
|
|
||||||
BackHandler(onBack = onClose)
|
|
||||||
Box(
|
|
||||||
// modifier = Modifier
|
|
||||||
// .background(MaterialTheme.colorScheme.su)
|
|
||||||
) {
|
|
||||||
Column {
|
|
||||||
Text(
|
|
||||||
text = headText,
|
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.CenterHorizontally)
|
|
||||||
.padding(20.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
LazyColumn(
|
|
||||||
modifier =
|
|
||||||
if (listOptions.size <=5) {
|
|
||||||
Modifier
|
|
||||||
.padding(20.dp, 20.dp, 20.dp, 0.dp)
|
|
||||||
.wrapContentSize()
|
|
||||||
}else {
|
|
||||||
Modifier
|
|
||||||
.padding(20.dp, 20.dp, 20.dp, 0.dp)
|
|
||||||
.height(400.dp)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
{
|
|
||||||
items(listOptions) { option ->
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable {
|
|
||||||
gotOption(option)
|
|
||||||
onClose()
|
|
||||||
},
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
RadioButtonComponent(value = selectedOption, onValueChange = {
|
|
||||||
gotOption(option)
|
|
||||||
onClose()
|
|
||||||
}, placeholder = option)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(10.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Row {
|
|
||||||
Spacer(Modifier.weight(1f))
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.cancel),
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(0.dp, 0.dp, 40.dp, 20.dp)
|
|
||||||
.clickable { onClose() },
|
|
||||||
color = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun RadioButtonComponent(value: String, onValueChange: (String) -> Unit, placeholder: String) {
|
|
||||||
RadioButton(
|
|
||||||
selected = value == placeholder,
|
|
||||||
onClick = { onValueChange(placeholder) }
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = placeholder,
|
|
||||||
modifier = Modifier.padding(start = 8.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
package `in`.sminnovations.attendanceapp.screens.settings.ui.components.settingsComponents
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
|
||||||
import `in`.sminnovations.attendanceapp.screens.settings.ui.components.settingsComponents.ClickableCard
|
|
||||||
import `in`.sminnovations.attendanceapp.screens.settings.ui.components.settingsComponents.RadioDialogue
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun RadioCard(
|
|
||||||
title:String,
|
|
||||||
subtitle:String,
|
|
||||||
icon: ImageVector,
|
|
||||||
radioHeader:String,
|
|
||||||
gotOption:(String)->Unit,
|
|
||||||
listOptions:List<String>,
|
|
||||||
selectedOption:String
|
|
||||||
) {
|
|
||||||
var showDialog by remember { mutableStateOf(false) }
|
|
||||||
ClickableCard(
|
|
||||||
title = title,
|
|
||||||
subtitle = subtitle,
|
|
||||||
onClick = {
|
|
||||||
showDialog = true
|
|
||||||
},
|
|
||||||
icon = icon
|
|
||||||
)
|
|
||||||
RadioDialogue(
|
|
||||||
showDialog = showDialog,
|
|
||||||
listOptions =listOptions,
|
|
||||||
headText =radioHeader ,
|
|
||||||
selectedOption = selectedOption,
|
|
||||||
onClose = { showDialog=false},
|
|
||||||
gotOption = gotOption
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
package `in`.sminnovations.attendanceapp.screens.settings.ui.components.subSetting
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.OutlinedCard
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.res.vectorResource
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import `in`.sminnovations.attendanceapp.R
|
|
||||||
import `in`.sminnovations.attendanceapp.screens.settings.data.SettingsData
|
|
||||||
import `in`.sminnovations.attendanceapp.screens.settings.ui.components.settingsComponents.RadioCard
|
|
||||||
import `in`.sminnovations.attendanceapp.screens.settings.ui.components.settingsComponents.SwitchCard
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun Appearance (context: Context) {
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
val dataStore = SettingsData(context)
|
|
||||||
val dynamicColorsThemeValue = dataStore.getDynamicColorThemeValue.collectAsState(true)
|
|
||||||
val blackThemeValue = dataStore.getBlackThemeValue.collectAsState(initial = false)
|
|
||||||
val theme = dataStore.getThemeMode.collectAsState(initial = "System default")
|
|
||||||
Column(modifier = Modifier.padding(10.dp, 0.dp, 10.dp, 0.dp)) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.appearance),
|
|
||||||
style = MaterialTheme.typography.labelLarge,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.Start)
|
|
||||||
.padding(14.dp),
|
|
||||||
color = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
|
|
||||||
OutlinedCard{
|
|
||||||
RadioCard(
|
|
||||||
title = stringResource(R.string.theme),
|
|
||||||
subtitle =theme.value ,
|
|
||||||
icon = ImageVector.vectorResource(id = R.drawable.round_color_lens_24),
|
|
||||||
radioHeader = stringResource(R.string.select_theme_mode) ,
|
|
||||||
gotOption = {
|
|
||||||
scope.launch {
|
|
||||||
dataStore.saveThemeMode(it)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
listOptions = listOf("Light", "Dark", "System default"),
|
|
||||||
selectedOption =theme.value
|
|
||||||
)
|
|
||||||
|
|
||||||
SwitchCard(
|
|
||||||
checkedValue = dynamicColorsThemeValue.value!!,
|
|
||||||
title = stringResource(R.string.dynamic_theme),
|
|
||||||
subtitle = stringResource(R.string.theme_based_on_wallpaper_color),
|
|
||||||
onSwitchChange =
|
|
||||||
{
|
|
||||||
scope.launch {
|
|
||||||
dataStore.setDynamicColorThemeValue(it)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
icon = ImageVector.vectorResource(id = R.drawable.round_auto_mode_24)
|
|
||||||
)
|
|
||||||
SwitchCard(
|
|
||||||
checkedValue = blackThemeValue.value!!,
|
|
||||||
title = stringResource(R.string.black_theme),
|
|
||||||
subtitle = stringResource(R.string.black_theme_description),
|
|
||||||
onSwitchChange =
|
|
||||||
{
|
|
||||||
scope.launch {
|
|
||||||
dataStore.setBlackThemeValue(it)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
icon = ImageVector.vectorResource(id = R.drawable.round_contrast_24)
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -53,6 +53,7 @@ fun Developer_settings(context: Context) {
|
|||||||
},
|
},
|
||||||
icon = ImageVector.vectorResource(R.drawable.round_developer_mode_24)
|
icon = ImageVector.vectorResource(R.drawable.round_developer_mode_24)
|
||||||
)
|
)
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
ClickableCard(
|
ClickableCard(
|
||||||
title = "Clear the Database",
|
title = "Clear the Database",
|
||||||
@@ -63,14 +64,6 @@ fun Developer_settings(context: Context) {
|
|||||||
icon = ImageVector.vectorResource(R.drawable.round_delete_sweep_24)
|
icon = ImageVector.vectorResource(R.drawable.round_delete_sweep_24)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
ClickableCard(
|
|
||||||
title = "Change the language",
|
|
||||||
subtitle = "Current language :English",
|
|
||||||
onClick = {
|
|
||||||
Toast.makeText(context, "Not implemented yet", Toast.LENGTH_SHORT).show()
|
|
||||||
},
|
|
||||||
icon = ImageVector.vectorResource(R.drawable.round_translate_24)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
</adaptive-icon>
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
</adaptive-icon>
|
|
||||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 982 B After Width: | Height: | Size: 926 B |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 950 B |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2.0 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 3.0 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.9 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 9.7 KiB |
4
app/src/main/res/values/ic_launcher_background.xml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#EBFFF3</color>
|
||||||
|
</resources>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
[versions]
|
[versions]
|
||||||
agp = "8.4.0"
|
agp = "8.5.0"
|
||||||
kotlin = "1.9.0"
|
kotlin = "1.9.0"
|
||||||
coreKtx = "1.13.1"
|
coreKtx = "1.13.1"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
|
|||||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
|||||||
#Fri Oct 04 14:44:43 IST 2024
|
#Fri Oct 04 14:44:43 IST 2024
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
@@ -16,8 +16,10 @@ dependencyResolutionManagement {
|
|||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
rootProject.name = "SMI Attendance"
|
rootProject.name = "SMI Attendance"
|
||||||
include(":app")
|
include(":app")
|
||||||
|
|||||||