Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ed17b3e07 | ||
|
|
c9db1cb727 | ||
|
|
5489aeb588 | ||
|
|
1cebe79071 | ||
|
|
aed649bf7d | ||
|
|
e2e5733d4b | ||
|
|
419e628d06 | ||
|
|
b269993e13 | ||
|
|
53149d0361 | ||
|
|
811f71049d | ||
|
|
9c3156a85a | ||
|
|
2867ecce4f | ||
|
|
6c90a1a183 | ||
|
|
43d6979fac | ||
|
|
43317574e9 | ||
|
|
b7d0fd32a9 | ||
|
|
a72c062b96 | ||
|
|
304f2cbe60 | ||
|
|
5d691e9b1b | ||
|
|
43c3137041 | ||
|
|
830d347606 | ||
|
|
27dd707e21 | ||
|
|
3e15b0df6b | ||
|
|
3bb2b58ce1 |
1
.idea/misc.xml
generated
1
.idea/misc.xml
generated
@@ -1,4 +1,3 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
|
||||||
|
|||||||
209
.idea/other.xml
generated
209
.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="OnePlus" />
|
||||||
|
<option name="codename" value="OP5552L1" />
|
||||||
|
<option name="id" value="OP5552L1" />
|
||||||
|
<option name="manufacturer" value="OnePlus" />
|
||||||
|
<option name="name" value="CPH2415" />
|
||||||
|
<option name="screenDensity" value="480" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2412" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
<PersistentDeviceSelectionData>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="34" />
|
<option name="api" value="34" />
|
||||||
<option name="brand" value="OPPO" />
|
<option name="brand" value="OPPO" />
|
||||||
@@ -47,6 +58,28 @@
|
|||||||
<option name="screenX" value="1840" />
|
<option name="screenX" value="1840" />
|
||||||
<option name="screenY" value="2944" />
|
<option name="screenY" value="2944" />
|
||||||
</PersistentDeviceSelectionData>
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="samsung" />
|
||||||
|
<option name="codename" value="a15" />
|
||||||
|
<option name="id" value="a15" />
|
||||||
|
<option name="manufacturer" value="Samsung" />
|
||||||
|
<option name="name" value="A15" />
|
||||||
|
<option name="screenDensity" value="450" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2340" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="samsung" />
|
||||||
|
<option name="codename" value="a35x" />
|
||||||
|
<option name="id" value="a35x" />
|
||||||
|
<option name="manufacturer" value="Samsung" />
|
||||||
|
<option name="name" value="A35" />
|
||||||
|
<option name="screenDensity" value="450" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2340" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
<PersistentDeviceSelectionData>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="31" />
|
<option name="api" value="31" />
|
||||||
<option name="brand" value="samsung" />
|
<option name="brand" value="samsung" />
|
||||||
@@ -69,6 +102,28 @@
|
|||||||
<option name="screenX" value="1080" />
|
<option name="screenX" value="1080" />
|
||||||
<option name="screenY" value="2400" />
|
<option name="screenY" value="2400" />
|
||||||
</PersistentDeviceSelectionData>
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="motorola" />
|
||||||
|
<option name="codename" value="arcfox" />
|
||||||
|
<option name="id" value="arcfox" />
|
||||||
|
<option name="manufacturer" value="Motorola" />
|
||||||
|
<option name="name" value="razr plus 2024" />
|
||||||
|
<option name="screenDensity" value="360" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="1272" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="33" />
|
||||||
|
<option name="brand" value="motorola" />
|
||||||
|
<option name="codename" value="austin" />
|
||||||
|
<option name="id" value="austin" />
|
||||||
|
<option name="manufacturer" value="Motorola" />
|
||||||
|
<option name="name" value="moto g 5G (2022)" />
|
||||||
|
<option name="screenDensity" value="280" />
|
||||||
|
<option name="screenX" value="720" />
|
||||||
|
<option name="screenY" value="1600" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
<PersistentDeviceSelectionData>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="33" />
|
<option name="api" value="33" />
|
||||||
<option name="brand" value="samsung" />
|
<option name="brand" value="samsung" />
|
||||||
@@ -124,6 +179,17 @@
|
|||||||
<option name="screenX" value="2220" />
|
<option name="screenX" value="2220" />
|
||||||
<option name="screenY" value="1080" />
|
<option name="screenY" value="1080" />
|
||||||
</PersistentDeviceSelectionData>
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="samsung" />
|
||||||
|
<option name="codename" value="dm2q" />
|
||||||
|
<option name="id" value="dm2q" />
|
||||||
|
<option name="manufacturer" value="Samsung" />
|
||||||
|
<option name="name" value="S23 Plus" />
|
||||||
|
<option name="screenDensity" value="450" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2340" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
<PersistentDeviceSelectionData>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="34" />
|
<option name="api" value="34" />
|
||||||
<option name="brand" value="samsung" />
|
<option name="brand" value="samsung" />
|
||||||
@@ -146,6 +212,28 @@
|
|||||||
<option name="screenX" value="1080" />
|
<option name="screenX" value="1080" />
|
||||||
<option name="screenY" value="2340" />
|
<option name="screenY" value="2340" />
|
||||||
</PersistentDeviceSelectionData>
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="samsung" />
|
||||||
|
<option name="codename" value="e3q" />
|
||||||
|
<option name="id" value="e3q" />
|
||||||
|
<option name="manufacturer" value="Samsung" />
|
||||||
|
<option name="name" value="Galaxy S24 Ultra" />
|
||||||
|
<option name="screenDensity" value="450" />
|
||||||
|
<option name="screenX" value="1440" />
|
||||||
|
<option name="screenY" value="3120" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="33" />
|
||||||
|
<option name="brand" value="google" />
|
||||||
|
<option name="codename" value="eos" />
|
||||||
|
<option name="id" value="eos" />
|
||||||
|
<option name="manufacturer" value="Google" />
|
||||||
|
<option name="name" value="Eos" />
|
||||||
|
<option name="screenDensity" value="320" />
|
||||||
|
<option name="screenX" value="384" />
|
||||||
|
<option name="screenY" value="384" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
<PersistentDeviceSelectionData>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="33" />
|
<option name="api" value="33" />
|
||||||
<option name="brand" value="google" />
|
<option name="brand" value="google" />
|
||||||
@@ -179,6 +267,50 @@
|
|||||||
<option name="screenX" value="2208" />
|
<option name="screenX" value="2208" />
|
||||||
<option name="screenY" value="1840" />
|
<option name="screenY" value="1840" />
|
||||||
</PersistentDeviceSelectionData>
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="motorola" />
|
||||||
|
<option name="codename" value="fogona" />
|
||||||
|
<option name="id" value="fogona" />
|
||||||
|
<option name="manufacturer" value="Motorola" />
|
||||||
|
<option name="name" value="moto g play - 2024" />
|
||||||
|
<option name="screenDensity" value="280" />
|
||||||
|
<option name="screenX" value="720" />
|
||||||
|
<option name="screenY" value="1600" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="samsung" />
|
||||||
|
<option name="codename" value="g0q" />
|
||||||
|
<option name="id" value="g0q" />
|
||||||
|
<option name="manufacturer" value="Samsung" />
|
||||||
|
<option name="name" value="SM-S906U1" />
|
||||||
|
<option name="screenDensity" value="450" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2340" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="samsung" />
|
||||||
|
<option name="codename" value="gta9pwifi" />
|
||||||
|
<option name="id" value="gta9pwifi" />
|
||||||
|
<option name="manufacturer" value="Samsung" />
|
||||||
|
<option name="name" value="SM-X210" />
|
||||||
|
<option name="screenDensity" value="240" />
|
||||||
|
<option name="screenX" value="1200" />
|
||||||
|
<option name="screenY" value="1920" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="samsung" />
|
||||||
|
<option name="codename" value="gts7xllite" />
|
||||||
|
<option name="id" value="gts7xllite" />
|
||||||
|
<option name="manufacturer" value="Samsung" />
|
||||||
|
<option name="name" value="SM-T738U" />
|
||||||
|
<option name="screenDensity" value="340" />
|
||||||
|
<option name="screenX" value="1600" />
|
||||||
|
<option name="screenY" value="2560" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
<PersistentDeviceSelectionData>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="33" />
|
<option name="api" value="33" />
|
||||||
<option name="brand" value="samsung" />
|
<option name="brand" value="samsung" />
|
||||||
@@ -190,6 +322,28 @@
|
|||||||
<option name="screenX" value="1848" />
|
<option name="screenX" value="1848" />
|
||||||
<option name="screenY" value="2960" />
|
<option name="screenY" value="2960" />
|
||||||
</PersistentDeviceSelectionData>
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="samsung" />
|
||||||
|
<option name="codename" value="gts8wifi" />
|
||||||
|
<option name="id" value="gts8wifi" />
|
||||||
|
<option name="manufacturer" value="Samsung" />
|
||||||
|
<option name="name" value="Galaxy Tab S8" />
|
||||||
|
<option name="screenDensity" value="274" />
|
||||||
|
<option name="screenX" value="1600" />
|
||||||
|
<option name="screenY" value="2560" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="samsung" />
|
||||||
|
<option name="codename" value="gts9fe" />
|
||||||
|
<option name="id" value="gts9fe" />
|
||||||
|
<option name="manufacturer" value="Samsung" />
|
||||||
|
<option name="name" value="Galaxy Tab S9 FE 5G" />
|
||||||
|
<option name="screenDensity" value="280" />
|
||||||
|
<option name="screenX" value="1440" />
|
||||||
|
<option name="screenY" value="2304" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
<PersistentDeviceSelectionData>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="34" />
|
<option name="api" value="34" />
|
||||||
<option name="brand" value="google" />
|
<option name="brand" value="google" />
|
||||||
@@ -234,6 +388,28 @@
|
|||||||
<option name="screenX" value="1080" />
|
<option name="screenX" value="1080" />
|
||||||
<option name="screenY" value="2400" />
|
<option name="screenY" value="2400" />
|
||||||
</PersistentDeviceSelectionData>
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="33" />
|
||||||
|
<option name="brand" value="motorola" />
|
||||||
|
<option name="codename" value="maui" />
|
||||||
|
<option name="id" value="maui" />
|
||||||
|
<option name="manufacturer" value="Motorola" />
|
||||||
|
<option name="name" value="moto g play - 2023" />
|
||||||
|
<option name="screenDensity" value="280" />
|
||||||
|
<option name="screenX" value="720" />
|
||||||
|
<option name="screenY" value="1600" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="samsung" />
|
||||||
|
<option name="codename" value="o1q" />
|
||||||
|
<option name="id" value="o1q" />
|
||||||
|
<option name="manufacturer" value="Samsung" />
|
||||||
|
<option name="name" value="Galaxy S21" />
|
||||||
|
<option name="screenDensity" value="421" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2400" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
<PersistentDeviceSelectionData>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="31" />
|
<option name="api" value="31" />
|
||||||
<option name="brand" value="google" />
|
<option name="brand" value="google" />
|
||||||
@@ -290,6 +466,17 @@
|
|||||||
<option name="screenY" value="384" />
|
<option name="screenY" value="384" />
|
||||||
<option name="type" value="WEAR_OS" />
|
<option name="type" value="WEAR_OS" />
|
||||||
</PersistentDeviceSelectionData>
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="samsung" />
|
||||||
|
<option name="codename" value="r11q" />
|
||||||
|
<option name="id" value="r11q" />
|
||||||
|
<option name="manufacturer" value="Samsung" />
|
||||||
|
<option name="name" value="SM-S711U" />
|
||||||
|
<option name="screenDensity" value="450" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2340" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
<PersistentDeviceSelectionData>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="30" />
|
<option name="api" value="30" />
|
||||||
<option name="brand" value="google" />
|
<option name="brand" value="google" />
|
||||||
@@ -312,6 +499,17 @@
|
|||||||
<option name="screenX" value="1080" />
|
<option name="screenX" value="1080" />
|
||||||
<option name="screenY" value="2400" />
|
<option name="screenY" value="2400" />
|
||||||
</PersistentDeviceSelectionData>
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="samsung" />
|
||||||
|
<option name="codename" value="t2q" />
|
||||||
|
<option name="id" value="t2q" />
|
||||||
|
<option name="manufacturer" value="Samsung" />
|
||||||
|
<option name="name" value="Galaxy S21 Plus" />
|
||||||
|
<option name="screenDensity" value="394" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2400" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
<PersistentDeviceSelectionData>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="33" />
|
<option name="api" value="33" />
|
||||||
<option name="brand" value="google" />
|
<option name="brand" value="google" />
|
||||||
@@ -334,6 +532,17 @@
|
|||||||
<option name="screenX" value="1080" />
|
<option name="screenX" value="1080" />
|
||||||
<option name="screenY" value="2424" />
|
<option name="screenY" value="2424" />
|
||||||
</PersistentDeviceSelectionData>
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="35" />
|
||||||
|
<option name="brand" value="google" />
|
||||||
|
<option name="codename" value="tokay" />
|
||||||
|
<option name="id" value="tokay" />
|
||||||
|
<option name="manufacturer" value="Google" />
|
||||||
|
<option name="name" value="Pixel 9" />
|
||||||
|
<option name="screenDensity" value="420" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2424" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
</list>
|
</list>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ android {
|
|||||||
applicationId "in.sminnovations.smiadmin"
|
applicationId "in.sminnovations.smiadmin"
|
||||||
minSdk 26
|
minSdk 26
|
||||||
targetSdk 34
|
targetSdk 34
|
||||||
versionCode 2
|
versionCode 7
|
||||||
versionName "1.1"
|
versionName "1.8-beta"
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
@@ -55,5 +55,6 @@ dependencies {
|
|||||||
implementation 'androidx.cardview:cardview:1.0.0'
|
implementation 'androidx.cardview:cardview:1.0.0'
|
||||||
implementation 'com.google.android.material:material:1.11.0'
|
implementation 'com.google.android.material:material:1.11.0'
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.3.2'
|
implementation 'androidx.recyclerview:recyclerview:1.3.2'
|
||||||
|
implementation 'com.google.zxing:core:3.4.1'
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -17,9 +17,9 @@
|
|||||||
android:requestLegacyExternalStorage="true"
|
android:requestLegacyExternalStorage="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@drawable/smi_logo"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@drawable/smi_logo"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.SMIAdmin"
|
android:theme="@style/Theme.SMIAdmin"
|
||||||
tools:targetApi="31">
|
tools:targetApi="31">
|
||||||
|
|||||||
81
app/src/main/java/in/sminnovations/smiadmin/BaseActivity.kt
Normal file
81
app/src/main/java/in/sminnovations/smiadmin/BaseActivity.kt
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
package `in`.sminnovations.smiadmin
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.Network
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
|
import android.net.NetworkRequest
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.provider.Settings
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import `in`.sminnovations.smiadmin.ui.utils.NetworkUtils
|
||||||
|
|
||||||
|
abstract class BaseActivity : AppCompatActivity() {
|
||||||
|
private var internetDialog: AlertDialog? = null
|
||||||
|
private lateinit var networkUtils: NetworkUtils
|
||||||
|
private lateinit var connectivityCallback: ConnectivityManager.NetworkCallback
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
networkUtils = NetworkUtils(this)
|
||||||
|
setupNetworkCallback()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupNetworkCallback() {
|
||||||
|
val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||||
|
connectivityCallback = object : ConnectivityManager.NetworkCallback() {
|
||||||
|
override fun onAvailable(network: Network) {
|
||||||
|
runOnUiThread {
|
||||||
|
internetDialog?.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLost(network: Network) {
|
||||||
|
runOnUiThread {
|
||||||
|
showNoInternetDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
connectivityManager.registerDefaultNetworkCallback(connectivityCallback)
|
||||||
|
} else {
|
||||||
|
val request = NetworkRequest.Builder()
|
||||||
|
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||||
|
.build()
|
||||||
|
connectivityManager.registerNetworkCallback(request, connectivityCallback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showNoInternetDialog() {
|
||||||
|
if (internetDialog?.isShowing == true) return
|
||||||
|
|
||||||
|
internetDialog = AlertDialog.Builder(this)
|
||||||
|
.setTitle("No Internet Connection")
|
||||||
|
.setMessage("Please turn on your mobile data or connect to Wi-Fi")
|
||||||
|
.setCancelable(false)
|
||||||
|
.setPositiveButton("Open Settings") { _, _ ->
|
||||||
|
startActivity(Intent(Settings.ACTION_WIRELESS_SETTINGS))
|
||||||
|
}
|
||||||
|
.create()
|
||||||
|
|
||||||
|
internetDialog?.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
if (!networkUtils.isInternetAvailable()) {
|
||||||
|
showNoInternetDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||||
|
connectivityManager.unregisterNetworkCallback(connectivityCallback)
|
||||||
|
internetDialog?.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package `in`.sminnovations.smiadmin
|
package `in`.sminnovations.smiadmin
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
@@ -12,7 +13,7 @@ import androidx.navigation.ui.setupWithNavController
|
|||||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||||
import `in`.sminnovations.smiadmin.databinding.ActivityMainBinding
|
import `in`.sminnovations.smiadmin.databinding.ActivityMainBinding
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : BaseActivity() {
|
||||||
private lateinit var binding: ActivityMainBinding
|
private lateinit var binding: ActivityMainBinding
|
||||||
private lateinit var navController: NavController
|
private lateinit var navController: NavController
|
||||||
|
|
||||||
@@ -29,7 +30,21 @@ class MainActivity : AppCompatActivity() {
|
|||||||
navController = navHostFragment.navController
|
navController = navHostFragment.navController
|
||||||
|
|
||||||
// Setup the bottom navigation with the NavController
|
// Setup the bottom navigation with the NavController
|
||||||
binding.navView.setupWithNavController(navController)
|
binding.navView?.let { bottomNav ->
|
||||||
|
bottomNav.setupWithNavController(navController)
|
||||||
|
|
||||||
|
// Hide bottom nav on specific destinations
|
||||||
|
navController.addOnDestinationChangedListener { _, destination, _ ->
|
||||||
|
when (destination.id) {
|
||||||
|
R.id.navigation_login -> {
|
||||||
|
bottomNav.visibility = View.GONE
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
bottomNav.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Set up navigation item selection listener
|
// Set up navigation item selection listener
|
||||||
binding.navView.setOnItemSelectedListener { menuItem ->
|
binding.navView.setOnItemSelectedListener { menuItem ->
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package `in`.sminnovations.smiadmin.data
|
|||||||
|
|
||||||
data class AttendanceData(
|
data class AttendanceData(
|
||||||
val date: String = "",
|
val date: String = "",
|
||||||
val checkIn: String = "",
|
val checkIns: Map<String, String>,
|
||||||
val checkOut: String = ""
|
val checkOuts: Map<String, String>,
|
||||||
|
var isExpanded: Boolean = false
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,41 @@
|
|||||||
package `in`.sminnovations.smiadmin.data
|
package `in`.sminnovations.smiadmin.data
|
||||||
|
|
||||||
|
import com.google.firebase.firestore.PropertyName
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
data class Employee(
|
data class Employee(
|
||||||
val Id: String = "",
|
@PropertyName("Id")
|
||||||
val Name: String = "",
|
var Id: String = "",
|
||||||
var EmployeeStatus: Boolean = false
|
|
||||||
|
@PropertyName("Name")
|
||||||
|
var Name: String = "",
|
||||||
|
|
||||||
|
@PropertyName("EmployeeStatus")
|
||||||
|
var EmployeeStatus: Boolean = true,
|
||||||
|
|
||||||
|
@PropertyName("Designation")
|
||||||
|
var Designation: String = "",
|
||||||
|
|
||||||
|
@PropertyName("DateOfJoining")
|
||||||
|
var DateOfJoining: String = "",
|
||||||
|
|
||||||
|
@PropertyName("dateOfBirth")
|
||||||
|
var dateOfBirth: String = "",
|
||||||
|
|
||||||
|
@PropertyName("Mobile")
|
||||||
|
var Mobile: String = "",
|
||||||
|
|
||||||
|
@PropertyName("Email")
|
||||||
|
var Email: String = "",
|
||||||
|
|
||||||
|
@PropertyName("CreatedAt")
|
||||||
|
var CreatedAt: String = "",
|
||||||
|
|
||||||
|
@PropertyName("from")
|
||||||
|
var from: String = "",
|
||||||
|
|
||||||
|
@PropertyName("role")
|
||||||
|
var role: String = "user"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
package `in`.sminnovations.smiadmin.ui.adapter
|
package `in`.sminnovations.smiadmin.ui.adapter
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.cardview.widget.CardView
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import `in`.sminnovations.smiadmin.R
|
import `in`.sminnovations.smiadmin.R
|
||||||
import `in`.sminnovations.smiadmin.data.AttendanceData
|
import `in`.sminnovations.smiadmin.data.AttendanceData
|
||||||
@@ -13,8 +17,9 @@ class AttendanceAdapter : RecyclerView.Adapter<AttendanceAdapter.ViewHolder>() {
|
|||||||
|
|
||||||
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
val dateTextView: TextView = itemView.findViewById(R.id.textViewDate)
|
val dateTextView: TextView = itemView.findViewById(R.id.textViewDate)
|
||||||
val checkInTextView: TextView = itemView.findViewById(R.id.textViewCheckIn)
|
val checkInsLayout: LinearLayout = itemView.findViewById(R.id.layoutCheckIns)
|
||||||
val checkOutTextView: TextView = itemView.findViewById(R.id.textViewCheckOut)
|
val expandableLayout: LinearLayout = itemView.findViewById(R.id.expandableLayout)
|
||||||
|
val cardView: CardView = itemView.findViewById(R.id.cardView)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
@@ -26,24 +31,164 @@ class AttendanceAdapter : RecyclerView.Adapter<AttendanceAdapter.ViewHolder>() {
|
|||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
val attendance = attendanceList[position]
|
val attendance = attendanceList[position]
|
||||||
holder.dateTextView.text = attendance.date
|
holder.dateTextView.text = attendance.date
|
||||||
holder.checkInTextView.text = "Check In: ${attendance.checkIn}"
|
|
||||||
holder.checkOutTextView.text = "Check Out: ${attendance.checkOut}"
|
// Clear previous views
|
||||||
|
holder.checkInsLayout.removeAllViews()
|
||||||
|
|
||||||
|
// Display all attendance records
|
||||||
|
displayAttendanceRecords(holder.itemView.context, holder, attendance)
|
||||||
|
|
||||||
|
// Set up expansion/collapse
|
||||||
|
holder.expandableLayout.apply {
|
||||||
|
visibility = if (attendance.isExpanded) View.VISIBLE else View.GONE
|
||||||
|
// Add animation for smooth expansion/collapse
|
||||||
|
alpha = if (attendance.isExpanded) 1.0f else 0.0f
|
||||||
|
animate()
|
||||||
|
.alpha(if (attendance.isExpanded) 1.0f else 0.0f)
|
||||||
|
.setDuration(200)
|
||||||
|
.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
holder.cardView.setOnClickListener {
|
||||||
|
// Toggle expansion state
|
||||||
|
val wasExpanded = attendance.isExpanded
|
||||||
|
attendance.isExpanded = !wasExpanded
|
||||||
|
|
||||||
|
// Animate the change
|
||||||
|
if (!wasExpanded) {
|
||||||
|
holder.expandableLayout.visibility = View.VISIBLE
|
||||||
|
holder.expandableLayout.alpha = 0.0f
|
||||||
|
holder.expandableLayout.animate()
|
||||||
|
.alpha(1.0f)
|
||||||
|
.setDuration(200)
|
||||||
|
.start()
|
||||||
|
} else {
|
||||||
|
holder.expandableLayout.animate()
|
||||||
|
.alpha(0.0f)
|
||||||
|
.setDuration(200)
|
||||||
|
.withEndAction {
|
||||||
|
if (!attendance.isExpanded) {
|
||||||
|
holder.expandableLayout.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyItemChanged(position)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount() = attendanceList.size
|
override fun getItemCount() = attendanceList.size
|
||||||
|
|
||||||
// In AttendanceAdapter.kt
|
|
||||||
fun updateAttendance(newAttendance: List<AttendanceData>) {
|
fun updateAttendance(newAttendance: List<AttendanceData>) {
|
||||||
|
val diffCallback = AttendanceDiffCallback(attendanceList, newAttendance)
|
||||||
|
val diffResult = DiffUtil.calculateDiff(diffCallback)
|
||||||
|
|
||||||
attendanceList.clear()
|
attendanceList.clear()
|
||||||
// Convert date string to comparable format and sort
|
attendanceList.addAll(newAttendance.sortedByDescending { it.getComparableDate() })
|
||||||
attendanceList.addAll(newAttendance.sortedByDescending {
|
|
||||||
// Convert dd-MM-yyyy to comparable format
|
diffResult.dispatchUpdatesTo(this)
|
||||||
val parts = it.date.split("-")
|
|
||||||
val day = parts[0].toInt()
|
|
||||||
val month = parts[1].toInt()
|
|
||||||
val year = parts[2].toInt()
|
|
||||||
String.format("%04d%02d%02d", year, month, day)
|
|
||||||
})
|
|
||||||
notifyDataSetChanged()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun displayAttendanceRecords(
|
||||||
|
context: Context,
|
||||||
|
holder: ViewHolder,
|
||||||
|
attendance: AttendanceData
|
||||||
|
) {
|
||||||
|
// Create a set of all unique check-in keys
|
||||||
|
val allCheckInKeys = attendance.checkIns.keys
|
||||||
|
|
||||||
|
// Handle numbered check-ins
|
||||||
|
for (i in 1..attendance.checkIns.size) {
|
||||||
|
val checkInKey = "checkIn$i"
|
||||||
|
val checkOutKey = "checkOut$i"
|
||||||
|
|
||||||
|
if (attendance.checkIns.containsKey(checkInKey)) {
|
||||||
|
addCheckInPair(
|
||||||
|
context,
|
||||||
|
holder,
|
||||||
|
checkInKey,
|
||||||
|
checkOutKey,
|
||||||
|
attendance.checkIns[checkInKey]!!,
|
||||||
|
attendance.checkOuts[checkOutKey]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle non-numbered check-ins
|
||||||
|
allCheckInKeys
|
||||||
|
.filter { !it.matches(Regex("checkIn\\d+")) }
|
||||||
|
.forEach { checkInKey ->
|
||||||
|
val baseKey = checkInKey.replace("checkIn", "")
|
||||||
|
val checkOutKey = "checkOut$baseKey"
|
||||||
|
|
||||||
|
addCheckInPair(
|
||||||
|
context,
|
||||||
|
holder,
|
||||||
|
checkInKey,
|
||||||
|
checkOutKey,
|
||||||
|
attendance.checkIns[checkInKey]!!,
|
||||||
|
attendance.checkOuts[checkOutKey]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addCheckInPair(
|
||||||
|
context: Context,
|
||||||
|
holder: ViewHolder,
|
||||||
|
checkInKey: String,
|
||||||
|
checkOutKey: String,
|
||||||
|
checkInValue: String,
|
||||||
|
checkOutValue: String?
|
||||||
|
) {
|
||||||
|
val pairLayout = LayoutInflater.from(context)
|
||||||
|
.inflate(R.layout.item_checkin_checkout_pair, holder.checkInsLayout, false)
|
||||||
|
|
||||||
|
val checkInView = pairLayout.findViewById<TextView>(R.id.textViewCheckIn)
|
||||||
|
val checkOutView = pairLayout.findViewById<TextView>(R.id.textViewCheckOut)
|
||||||
|
|
||||||
|
// Display the original key name for non-standard keys
|
||||||
|
val checkInLabel = if (checkInKey.matches(Regex("checkIn\\d+"))) {
|
||||||
|
"Check In"
|
||||||
|
} else {
|
||||||
|
"Check In (${checkInKey.replace("checkIn", "")})"
|
||||||
|
}
|
||||||
|
|
||||||
|
val checkOutLabel = if (checkOutKey.matches(Regex("checkOut\\d+"))) {
|
||||||
|
"Check Out"
|
||||||
|
} else {
|
||||||
|
"Check Out (${checkOutKey.replace("checkOut", "")})"
|
||||||
|
}
|
||||||
|
|
||||||
|
checkInView.text = "$checkInLabel: $checkInValue"
|
||||||
|
checkOutView.text = "$checkOutLabel: ${checkOutValue ?: "Pending"}"
|
||||||
|
|
||||||
|
holder.checkInsLayout.addView(pairLayout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DiffUtil callback for efficient updates
|
||||||
|
private class AttendanceDiffCallback(
|
||||||
|
private val oldList: List<AttendanceData>,
|
||||||
|
private val newList: List<AttendanceData>
|
||||||
|
) : DiffUtil.Callback() {
|
||||||
|
override fun getOldListSize() = oldList.size
|
||||||
|
override fun getNewListSize() = newList.size
|
||||||
|
|
||||||
|
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||||
|
return oldList[oldItemPosition].date == newList[newItemPosition].date
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||||
|
return oldList[oldItemPosition] == newList[newItemPosition]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extension function for AttendanceData to get comparable date
|
||||||
|
private fun AttendanceData.getComparableDate(): String {
|
||||||
|
val parts = date.split("-")
|
||||||
|
val day = parts[0].toInt()
|
||||||
|
val month = parts[1].toInt()
|
||||||
|
val year = parts[2].toInt()
|
||||||
|
return String.format("%04d%02d%02d", year, month, day)
|
||||||
}
|
}
|
||||||
@@ -10,7 +10,7 @@ import `in`.sminnovations.smiadmin.R
|
|||||||
import `in`.sminnovations.smiadmin.data.Employee
|
import `in`.sminnovations.smiadmin.data.Employee
|
||||||
|
|
||||||
class EmployeeListAdapter(
|
class EmployeeListAdapter(
|
||||||
private val employees: MutableList<Employee> = mutableListOf(),
|
val employees: MutableList<Employee> = mutableListOf(),
|
||||||
private val onStatusChanged: (Employee, Boolean) -> Unit
|
private val onStatusChanged: (Employee, Boolean) -> Unit
|
||||||
) : RecyclerView.Adapter<EmployeeListAdapter.EmployeeViewHolder>() {
|
) : RecyclerView.Adapter<EmployeeListAdapter.EmployeeViewHolder>() {
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ class EmployeeListAdapter(
|
|||||||
holder.statusSwitch.setOnCheckedChangeListener(null)
|
holder.statusSwitch.setOnCheckedChangeListener(null)
|
||||||
|
|
||||||
// Set the text and switch state
|
// Set the text and switch state
|
||||||
holder.nameTextView.text = employee.Name
|
holder.nameTextView.text = employee.Name + " (" + employee.from + ")"
|
||||||
holder.statusSwitch.isChecked = employee.EmployeeStatus
|
holder.statusSwitch.isChecked = employee.EmployeeStatus
|
||||||
|
|
||||||
// Set up the new listener after setting the checked state
|
// Set up the new listener after setting the checked state
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package `in`.sminnovations.smiadmin.ui.adapter
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import `in`.sminnovations.smiadmin.data.Employee
|
||||||
|
import `in`.sminnovations.smiadmin.databinding.ItemEmployeeQrBinding
|
||||||
|
|
||||||
|
// EmployeeAdapter.kt (remains the same)
|
||||||
|
class EmployeeQrAdapter(
|
||||||
|
private val onShareClick: (Employee) -> Unit,
|
||||||
|
private val onItemClick: (Employee) -> Unit
|
||||||
|
) : ListAdapter<Employee, EmployeeQrAdapter.EmployeeViewHolder>(EmployeeDiffCallback()) {
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EmployeeViewHolder {
|
||||||
|
val binding = ItemEmployeeQrBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
return EmployeeViewHolder(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: EmployeeViewHolder, position: Int) {
|
||||||
|
holder.bind(getItem(position))
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class EmployeeViewHolder(
|
||||||
|
private val binding: ItemEmployeeQrBinding
|
||||||
|
) : RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
|
fun bind(employee: Employee) {
|
||||||
|
binding.apply {
|
||||||
|
textEmployeeName.text = employee.Name + " (" + employee.from + ")"
|
||||||
|
textEmployeeId.text = employee.Id
|
||||||
|
|
||||||
|
buttonShare.setOnClickListener { onShareClick(employee) }
|
||||||
|
root.setOnClickListener { onItemClick(employee) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EmployeeDiffCallback.kt (remains the same)
|
||||||
|
class EmployeeDiffCallback : DiffUtil.ItemCallback<Employee>() {
|
||||||
|
override fun areItemsTheSame(oldItem: Employee, newItem: Employee): Boolean {
|
||||||
|
return oldItem.Id == newItem.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: Employee, newItem: Employee): Boolean {
|
||||||
|
return oldItem == newItem
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,7 +26,7 @@ class EmployeeStatusListAdapter(
|
|||||||
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
val employee = employees[position]
|
val employee = employees[position]
|
||||||
holder.nameTextView.text = employee.Name
|
holder.nameTextView.text = employee.Name + " (" + employee.from + ")"
|
||||||
holder.itemView.setOnClickListener {
|
holder.itemView.setOnClickListener {
|
||||||
onEmployeeClick(employee)
|
onEmployeeClick(employee)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,189 @@
|
|||||||
|
package `in`.sminnovations.smiadmin.ui.fragments
|
||||||
|
|
||||||
|
import android.app.DatePickerDialog
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import com.google.firebase.firestore.FirebaseFirestore
|
||||||
|
import `in`.sminnovations.smiadmin.R
|
||||||
|
import `in`.sminnovations.smiadmin.data.Employee
|
||||||
|
import `in`.sminnovations.smiadmin.databinding.FragmentAddEmployeeBinding
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class AddEmployeeFragment : Fragment() {
|
||||||
|
|
||||||
|
private var _binding: FragmentAddEmployeeBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
private val db = FirebaseFirestore.getInstance()
|
||||||
|
private var dateOfBirth: Calendar? = null
|
||||||
|
private var dateOfJoining: Calendar? = null
|
||||||
|
private val dateFormatter = SimpleDateFormat("dd/MM/yyyy", Locale.getDefault())
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
_binding = FragmentAddEmployeeBinding.inflate(inflater, container, false)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
val items = arrayOf("IISc", "SMI")
|
||||||
|
val adapter = ArrayAdapter(requireActivity(), R.layout.list_item, items)
|
||||||
|
binding.etEmployeeFrom.setAdapter(adapter)
|
||||||
|
|
||||||
|
binding.etEmployeeFrom.isFocusable = false
|
||||||
|
binding.etEmployeeFrom.isClickable = true
|
||||||
|
setupDatePickers()
|
||||||
|
setupSubmitButton()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupDatePickers() {
|
||||||
|
binding.etDateOfBirth.setOnClickListener {
|
||||||
|
showDatePicker(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.etDateOfJoining.setOnClickListener {
|
||||||
|
showDatePicker(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showDatePicker(isDateOfBirth: Boolean) {
|
||||||
|
val calendar = Calendar.getInstance()
|
||||||
|
|
||||||
|
DatePickerDialog(
|
||||||
|
requireContext(),
|
||||||
|
{ _, year, month, day ->
|
||||||
|
calendar.set(year, month, day)
|
||||||
|
val formattedDate = dateFormatter.format(calendar.time)
|
||||||
|
|
||||||
|
if (isDateOfBirth) {
|
||||||
|
dateOfBirth = calendar.clone() as Calendar
|
||||||
|
binding.etDateOfBirth.setText(formattedDate)
|
||||||
|
} else {
|
||||||
|
dateOfJoining = calendar.clone() as Calendar
|
||||||
|
binding.etDateOfJoining.setText(formattedDate)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
calendar.get(Calendar.YEAR),
|
||||||
|
calendar.get(Calendar.MONTH),
|
||||||
|
calendar.get(Calendar.DAY_OF_MONTH)
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateEmployeeId(name: String, phone: String): String {
|
||||||
|
val namePrefix = name.take(4).lowercase()
|
||||||
|
val phoneSuffix = phone.takeLast(4)
|
||||||
|
return "$namePrefix$phoneSuffix"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun validateInputs(): Boolean {
|
||||||
|
var isValid = true
|
||||||
|
|
||||||
|
with(binding) {
|
||||||
|
if (etEmployeeName.text.isNullOrBlank()) {
|
||||||
|
etEmployeeName.error = "Name is required"
|
||||||
|
isValid = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (etEmployeeEmail.text.isNullOrBlank()) {
|
||||||
|
etEmployeeEmail.error = "Email is required"
|
||||||
|
isValid = false
|
||||||
|
} else if (!android.util.Patterns.EMAIL_ADDRESS.matcher(etEmployeeEmail.text.toString()).matches()) {
|
||||||
|
etEmployeeEmail.error = "Invalid email format"
|
||||||
|
isValid = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (etEmployeePhone.text.isNullOrBlank()) {
|
||||||
|
etEmployeePhone.error = "Phone number is required"
|
||||||
|
isValid = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (etEmployeePosition.text.isNullOrBlank()) {
|
||||||
|
etEmployeePosition.error = "Position is required"
|
||||||
|
isValid = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dateOfBirth == null) {
|
||||||
|
etDateOfBirth.error = "Date of Birth is required"
|
||||||
|
isValid = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dateOfJoining == null) {
|
||||||
|
etDateOfJoining.error = "Date of Joining is required"
|
||||||
|
isValid = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValid
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveEmployee() {
|
||||||
|
binding.btnSubmit.isEnabled = false // Prevent double submission
|
||||||
|
|
||||||
|
val name = binding.etEmployeeName.text.toString().trim()
|
||||||
|
val phone = binding.etEmployeePhone.text.toString().trim()
|
||||||
|
val employeeId = generateEmployeeId(name, phone)
|
||||||
|
|
||||||
|
// val employee = Employee(
|
||||||
|
// Id = employeeId,
|
||||||
|
// Name = name,
|
||||||
|
// Email = binding.etEmployeeEmail.text.toString().trim(),
|
||||||
|
// Mobile = phone,
|
||||||
|
// Designation = binding.etEmployeePosition.text.toString().trim(),
|
||||||
|
// dateOfBirth = dateFormatter.format(dateOfBirth?.time ?: Date()),
|
||||||
|
// DateOfJoining = dateFormatter.format(dateOfJoining?.time ?: Date()),
|
||||||
|
// CreatedAt = dateFormatter.format(Date()), // Current date in dd/MM/yyyy format
|
||||||
|
// from = binding.etEmployeeFrom.text.toString().trim(),
|
||||||
|
// )
|
||||||
|
val employee = hashMapOf(
|
||||||
|
"Id" to employeeId,
|
||||||
|
"Name" to name,
|
||||||
|
"EmployeeStatus" to true,
|
||||||
|
"Email" to binding.etEmployeeEmail.text.toString().trim(),
|
||||||
|
"Mobile" to phone,
|
||||||
|
"Designation" to binding.etEmployeePosition.text.toString().trim(),
|
||||||
|
"dateOfBirth" to dateFormatter.format(dateOfBirth?.time ?: Date()),
|
||||||
|
"DateOfJoining" to dateFormatter.format(dateOfJoining?.time ?: Date()),
|
||||||
|
"CreatedAt" to dateFormatter.format(Date()),
|
||||||
|
"from" to binding.etEmployeeFrom.text.toString().trim(),
|
||||||
|
"role" to "user"
|
||||||
|
)
|
||||||
|
|
||||||
|
db.collection("employees")
|
||||||
|
.document(employeeId)
|
||||||
|
.set(employee)
|
||||||
|
.addOnSuccessListener {
|
||||||
|
Toast.makeText(context, "Employee added successfully", Toast.LENGTH_SHORT).show()
|
||||||
|
findNavController().navigateUp()
|
||||||
|
}
|
||||||
|
.addOnFailureListener { e ->
|
||||||
|
binding.btnSubmit.isEnabled = true
|
||||||
|
Toast.makeText(context, "Error: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupSubmitButton() {
|
||||||
|
binding.btnSubmit.setOnClickListener {
|
||||||
|
if (validateInputs()) {
|
||||||
|
saveEmployee()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
_binding = null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
package `in`.sminnovations.smiadmin.ui.fragments
|
package `in`.sminnovations.smiadmin.ui.fragments
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
import android.app.AlertDialog
|
||||||
import android.app.ProgressDialog
|
import android.app.ProgressDialog
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
@@ -12,6 +14,8 @@ import android.provider.Settings
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.RadioGroup
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.cardview.widget.CardView
|
import androidx.cardview.widget.CardView
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
@@ -19,12 +23,16 @@ import androidx.fragment.app.Fragment
|
|||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import `in`.sminnovations.smiadmin.data.Constant.STORAGE_PERMISSION_CODE
|
import `in`.sminnovations.smiadmin.data.Constant.STORAGE_PERMISSION_CODE
|
||||||
import `in`.sminnovations.smiadmin.R
|
import `in`.sminnovations.smiadmin.R
|
||||||
|
import `in`.sminnovations.smiadmin.ui.utils.DailyMonthYearPickerDialog
|
||||||
import `in`.sminnovations.smiadmin.ui.utils.ExportTask
|
import `in`.sminnovations.smiadmin.ui.utils.ExportTask
|
||||||
import `in`.sminnovations.smiadmin.ui.utils.MonthYearPickerDialog
|
import `in`.sminnovations.smiadmin.ui.utils.MonthYearPickerDialog
|
||||||
|
import `in`.sminnovations.smiadmin.ui.utils.dailyReportExport
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
import java.util.Calendar
|
||||||
|
|
||||||
class HomeFragment : Fragment() {
|
class HomeFragment : Fragment() {
|
||||||
private lateinit var exportTask: ExportTask
|
private lateinit var exportTask: ExportTask
|
||||||
|
private lateinit var dailyReportExport: dailyReportExport
|
||||||
private var currentExcelTask: Job? = null
|
private var currentExcelTask: Job? = null
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
@@ -33,9 +41,10 @@ class HomeFragment : Fragment() {
|
|||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View? {
|
): View? {
|
||||||
exportTask = ExportTask(this)
|
exportTask = ExportTask(this)
|
||||||
|
dailyReportExport = dailyReportExport(this)
|
||||||
|
|
||||||
return inflater.inflate(R.layout.fragment_home, container, false)
|
return inflater.inflate(R.layout.fragment_home, container, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
@@ -46,7 +55,12 @@ class HomeFragment : Fragment() {
|
|||||||
view.findViewById<CardView>(R.id.cardAttendanceReport).setOnClickListener {
|
view.findViewById<CardView>(R.id.cardAttendanceReport).setOnClickListener {
|
||||||
findNavController().navigate(R.id.action_home_to_employee_attendance_list)
|
findNavController().navigate(R.id.action_home_to_employee_attendance_list)
|
||||||
}
|
}
|
||||||
|
view.findViewById<CardView>(R.id.cardAddEmployee).setOnClickListener {
|
||||||
|
findNavController().navigate(R.id.action_home_to_add_employee)
|
||||||
|
}
|
||||||
|
view.findViewById<CardView>(R.id.cardEmployeeQr).setOnClickListener {
|
||||||
|
findNavController().navigate(R.id.action_to_employeeQrFragment)
|
||||||
|
}
|
||||||
view.findViewById<CardView>(R.id.cardDownloadReport).setOnClickListener {
|
view.findViewById<CardView>(R.id.cardDownloadReport).setOnClickListener {
|
||||||
if (checkStoragePermission()) {
|
if (checkStoragePermission()) {
|
||||||
showMonthYearPicker()
|
showMonthYearPicker()
|
||||||
@@ -54,17 +68,60 @@ class HomeFragment : Fragment() {
|
|||||||
requestStoragePermission()
|
requestStoragePermission()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
view.findViewById<CardView>(R.id.cardDownloadDailyReport).setOnClickListener{
|
||||||
|
if(checkStoragePermission()){
|
||||||
|
showDailyMonthYearPicker()
|
||||||
|
}else{
|
||||||
|
requestStoragePermission()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showMonthYearPicker() {
|
private fun showMonthYearPicker() {
|
||||||
MonthYearPickerDialog().apply {
|
MonthYearPickerDialog().apply {
|
||||||
setOnDateSelectedListener { year, month ->
|
setOnDateSelectedListener { year, month ->
|
||||||
downloadAttendanceReport(year, month)
|
showReportDialog(year, month, requireActivity())
|
||||||
|
//downloadAttendanceReport(year, month)
|
||||||
}
|
}
|
||||||
}.show(parentFragmentManager, "MonthYearPicker")
|
}.show(parentFragmentManager, "MonthYearPicker")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun downloadAttendanceReport(year: Int, month: Int) {
|
|
||||||
|
private fun showDailyMonthYearPicker(){
|
||||||
|
DailyMonthYearPickerDialog().apply {
|
||||||
|
setOnDateSelectedListener {
|
||||||
|
showDailyReportDialog(it,requireContext())
|
||||||
|
}
|
||||||
|
}.show(parentFragmentManager,"Daily Date Picker")
|
||||||
|
}
|
||||||
|
private fun showReportDialog(year: Int, month: Int, context: Context) {
|
||||||
|
val dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_report_source, null)
|
||||||
|
val dialog = AlertDialog.Builder(context)
|
||||||
|
.setView(dialogView)
|
||||||
|
.setCancelable(true)
|
||||||
|
.create()
|
||||||
|
|
||||||
|
val radioGroup = dialogView.findViewById<RadioGroup>(R.id.rgSource)
|
||||||
|
val downloadButton = dialogView.findViewById<Button>(R.id.btnDownload)
|
||||||
|
|
||||||
|
downloadButton.setOnClickListener {
|
||||||
|
val selectedSource = when (radioGroup.checkedRadioButtonId) {
|
||||||
|
R.id.rbSMI -> "SMI"
|
||||||
|
R.id.rbIISC -> "IISc"
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedSource != null) {
|
||||||
|
downloadAttendanceReport(year, month, selectedSource)
|
||||||
|
dialog.dismiss()
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, "Please select a source", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.show()
|
||||||
|
}
|
||||||
|
private fun downloadAttendanceReport(year: Int, month: Int, source: String) {
|
||||||
currentExcelTask?.cancel()
|
currentExcelTask?.cancel()
|
||||||
|
|
||||||
val progressDialog = ProgressDialog(requireContext()).apply {
|
val progressDialog = ProgressDialog(requireContext()).apply {
|
||||||
@@ -72,10 +129,23 @@ class HomeFragment : Fragment() {
|
|||||||
setCancelable(false)
|
setCancelable(false)
|
||||||
show()
|
show()
|
||||||
}
|
}
|
||||||
exportTask.exportAttendance(year, month)
|
exportTask.exportAttendance(year, month,source)
|
||||||
progressDialog.dismiss()
|
progressDialog.dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun downloadAttendanceDailyReport(date : Calendar, source: String) {
|
||||||
|
currentExcelTask?.cancel()
|
||||||
|
|
||||||
|
val progressDialog = ProgressDialog(requireContext()).apply {
|
||||||
|
setMessage("Generating report...")
|
||||||
|
setCancelable(false)
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
dailyReportExport.exportAttendance(date =date,source =source)
|
||||||
|
progressDialog.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun checkStoragePermission(): Boolean {
|
private fun checkStoragePermission(): Boolean {
|
||||||
return if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
|
return if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
|
||||||
ContextCompat.checkSelfPermission(
|
ContextCompat.checkSelfPermission(
|
||||||
@@ -118,6 +188,7 @@ class HomeFragment : Fragment() {
|
|||||||
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
// Permission granted, now show the picker
|
// Permission granted, now show the picker
|
||||||
showMonthYearPicker()
|
showMonthYearPicker()
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
requireContext(),
|
requireContext(),
|
||||||
@@ -127,8 +198,36 @@ class HomeFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
private fun showDailyReportDialog(date : Calendar, context: Context) {
|
||||||
|
val dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_report_source, null)
|
||||||
|
val dialog = AlertDialog.Builder(context)
|
||||||
|
.setView(dialogView)
|
||||||
|
.setCancelable(true)
|
||||||
|
.create()
|
||||||
|
|
||||||
|
val radioGroup = dialogView.findViewById<RadioGroup>(R.id.rgSource)
|
||||||
|
val downloadButton = dialogView.findViewById<Button>(R.id.btnDownload)
|
||||||
|
|
||||||
|
downloadButton.setOnClickListener {
|
||||||
|
val selectedSource = when (radioGroup.checkedRadioButtonId) {
|
||||||
|
R.id.rbSMI -> "SMI"
|
||||||
|
R.id.rbIISC -> "IISc"
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedSource != null) {
|
||||||
|
downloadAttendanceDailyReport(date, selectedSource)
|
||||||
|
dialog.dismiss()
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, "Please select a source", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.show()
|
||||||
|
}
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
dailyReportExport.cleanup()
|
||||||
exportTask.cleanup()
|
exportTask.cleanup()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
package `in`.sminnovations.smiadmin.ui.fragments
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||||
|
import com.google.firebase.firestore.FirebaseFirestore
|
||||||
|
import `in`.sminnovations.smiadmin.MainActivity
|
||||||
|
import `in`.sminnovations.smiadmin.R
|
||||||
|
import `in`.sminnovations.smiadmin.databinding.FragmentLoginBinding
|
||||||
|
|
||||||
|
class LoginFragment : Fragment() {
|
||||||
|
private var _binding: FragmentLoginBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
private lateinit var db: FirebaseFirestore
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
_binding = FragmentLoginBinding.inflate(inflater, container, false)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
db = FirebaseFirestore.getInstance()
|
||||||
|
|
||||||
|
// Check if already logged in
|
||||||
|
val prefs = requireActivity().getSharedPreferences("admin_prefs", Context.MODE_PRIVATE)
|
||||||
|
val isLoggedIn = prefs.getBoolean("is_logged_in", false)
|
||||||
|
if (isLoggedIn) {
|
||||||
|
// Verify stored credentials in Firestore
|
||||||
|
binding.childLayout.visibility = View.GONE
|
||||||
|
binding.parentLayout.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
val adminMobile = prefs.getString("admin_mobile", "") ?: ""
|
||||||
|
verifyAndNavigate(adminMobile)
|
||||||
|
} else {
|
||||||
|
binding.childLayout.visibility = View.VISIBLE
|
||||||
|
binding.parentLayout.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
binding.btnLogin.setOnClickListener {
|
||||||
|
val mobile = binding.etMobile.text.toString().trim()
|
||||||
|
val password = binding.etPassword.text.toString().trim()
|
||||||
|
|
||||||
|
when {
|
||||||
|
mobile.isEmpty() -> {
|
||||||
|
binding.etMobile.error = "Please enter mobile number"
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
mobile.length != 10 -> {
|
||||||
|
binding.etMobile.error = "Please enter valid mobile number"
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
password.isEmpty() -> {
|
||||||
|
binding.etPassword.error = "Please enter password"
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
else -> attemptLogin(mobile, password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun verifyAndNavigate(mobile: String) {
|
||||||
|
if (mobile.isEmpty()) {
|
||||||
|
// If stored mobile is empty, clear prefs and show login
|
||||||
|
requireActivity().getSharedPreferences("admin_prefs", Context.MODE_PRIVATE)
|
||||||
|
.edit()
|
||||||
|
.clear()
|
||||||
|
.apply()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
db.collection("employees")
|
||||||
|
.whereEqualTo("mobile", mobile)
|
||||||
|
.whereEqualTo("role", "admin")
|
||||||
|
.get()
|
||||||
|
.addOnSuccessListener { documents ->
|
||||||
|
if (!documents.isEmpty) {
|
||||||
|
// Still valid admin, navigate to home
|
||||||
|
navigateToHome()
|
||||||
|
} else {
|
||||||
|
// No longer an admin, clear prefs
|
||||||
|
requireActivity().getSharedPreferences("admin_prefs", Context.MODE_PRIVATE)
|
||||||
|
.edit()
|
||||||
|
.clear()
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.addOnFailureListener {
|
||||||
|
// On error, stay on login screen
|
||||||
|
showError("Error verifying credentials")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun attemptLogin(mobile: String, password: String) {
|
||||||
|
showLoading(true)
|
||||||
|
|
||||||
|
db.collection("employees")
|
||||||
|
.whereEqualTo("mobile", mobile)
|
||||||
|
.whereEqualTo("role", "admin")
|
||||||
|
.get()
|
||||||
|
.addOnSuccessListener { documents ->
|
||||||
|
if (documents.isEmpty) {
|
||||||
|
showLoading(false)
|
||||||
|
showError("Invalid credentials or not an admin user")
|
||||||
|
return@addOnSuccessListener
|
||||||
|
}
|
||||||
|
|
||||||
|
val document = documents.first()
|
||||||
|
val storedPassword = document.getString("password") ?: document.getString("Password")
|
||||||
|
|
||||||
|
if (storedPassword == password) {
|
||||||
|
// Save login state
|
||||||
|
val editor = requireActivity().getSharedPreferences(
|
||||||
|
"admin_prefs",
|
||||||
|
Context.MODE_PRIVATE
|
||||||
|
).edit()
|
||||||
|
editor.putBoolean("is_logged_in", true)
|
||||||
|
editor.putString("admin_id", document.id)
|
||||||
|
editor.putString("admin_mobile", mobile)
|
||||||
|
editor.apply()
|
||||||
|
|
||||||
|
showLoading(false)
|
||||||
|
navigateToHome()
|
||||||
|
} else {
|
||||||
|
showLoading(false)
|
||||||
|
showError("Invalid password")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.addOnFailureListener { e ->
|
||||||
|
showLoading(false)
|
||||||
|
showError("Error: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showLoading(show: Boolean) {
|
||||||
|
binding.progressBar.visibility = if (show) View.VISIBLE else View.GONE
|
||||||
|
binding.btnLogin.isEnabled = !show
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showError(message: String) {
|
||||||
|
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun navigateToHome() {
|
||||||
|
findNavController().navigate(R.id.action_login_to_home)
|
||||||
|
}
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
// Hide bottom navigation
|
||||||
|
(activity as? MainActivity)?.findViewById<BottomNavigationView>(R.id.nav_view)?.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
super.onStop()
|
||||||
|
// Show bottom navigation when leaving fragment
|
||||||
|
(activity as? MainActivity)?.findViewById<BottomNavigationView>(R.id.nav_view)?.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
_binding = null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package `in`.sminnovations.smiadmin.ui.fragments
|
package `in`.sminnovations.smiadmin.ui.fragments
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
@@ -7,6 +8,8 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import com.google.android.material.button.MaterialButton
|
||||||
import `in`.sminnovations.smiadmin.R
|
import `in`.sminnovations.smiadmin.R
|
||||||
|
|
||||||
|
|
||||||
@@ -20,6 +23,10 @@ class SettingsFragment : Fragment() {
|
|||||||
|
|
||||||
// Set app version
|
// Set app version
|
||||||
val versionTextView: TextView = root.findViewById(R.id.textViewVersion)
|
val versionTextView: TextView = root.findViewById(R.id.textViewVersion)
|
||||||
|
val button: MaterialButton = root.findViewById(R.id.logOutBtn)
|
||||||
|
button.setOnClickListener {
|
||||||
|
logout()
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
val pInfo = requireContext().packageManager.getPackageInfo(
|
val pInfo = requireContext().packageManager.getPackageInfo(
|
||||||
requireContext().packageName, 0
|
requireContext().packageName, 0
|
||||||
@@ -34,4 +41,12 @@ class SettingsFragment : Fragment() {
|
|||||||
|
|
||||||
return root
|
return root
|
||||||
}
|
}
|
||||||
|
private fun logout() {
|
||||||
|
requireActivity().getSharedPreferences("admin_prefs", Context.MODE_PRIVATE)
|
||||||
|
.edit()
|
||||||
|
.clear()
|
||||||
|
.apply()
|
||||||
|
|
||||||
|
findNavController().navigate(R.id.action_settings_to_login)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
package `in`.sminnovations.smiadmin.ui.fragments
|
||||||
|
|
||||||
|
import android.R
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.AdapterView
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import com.google.firebase.firestore.FirebaseFirestore
|
||||||
|
import com.google.zxing.BarcodeFormat
|
||||||
|
import com.google.zxing.qrcode.QRCodeWriter
|
||||||
|
import `in`.sminnovations.smiadmin.data.Employee
|
||||||
|
import `in`.sminnovations.smiadmin.databinding.FragmentShareQrBinding
|
||||||
|
import `in`.sminnovations.smiadmin.ui.adapter.EmployeeQrAdapter
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
|
||||||
|
class ShareQrFragment : Fragment() {
|
||||||
|
|
||||||
|
private lateinit var binding: FragmentShareQrBinding
|
||||||
|
private lateinit var employeeAdapter: EmployeeQrAdapter
|
||||||
|
private val db = FirebaseFirestore.getInstance()
|
||||||
|
private var employeeList = emptyList<Employee>()
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
binding = FragmentShareQrBinding.inflate(inflater, container, false)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
setupRecyclerView()
|
||||||
|
setupSpinner()
|
||||||
|
setupSearchView()
|
||||||
|
fetchEmployees()
|
||||||
|
}
|
||||||
|
private fun setupSearchView() {
|
||||||
|
binding.searchView.setOnQueryTextListener(object : androidx.appcompat.widget.SearchView.OnQueryTextListener {
|
||||||
|
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onQueryTextChange(newText: String?): Boolean {
|
||||||
|
filterEmployees(newText)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun filterEmployees(query: String?) {
|
||||||
|
if (query.isNullOrBlank()) {
|
||||||
|
employeeAdapter.submitList(employeeList)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val filteredList = employeeList.filter {
|
||||||
|
it.Name.contains(query, ignoreCase = true)
|
||||||
|
}
|
||||||
|
employeeAdapter.submitList(filteredList)
|
||||||
|
}
|
||||||
|
private fun setupSpinner() {
|
||||||
|
val sortOptions = arrayOf("Name (A-Z)", "SMI First", "IISc First")
|
||||||
|
val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, sortOptions)
|
||||||
|
adapter.setDropDownViewResource(R.layout.simple_spinner_dropdown_item)
|
||||||
|
|
||||||
|
binding.spinnerSort.adapter = adapter
|
||||||
|
binding.spinnerSort.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||||
|
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||||
|
sortEmployees()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNothingSelected(parent: AdapterView<*>?) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private fun sortEmployees() {
|
||||||
|
val currentList = employeeAdapter.currentList.toMutableList()
|
||||||
|
when (binding.spinnerSort.selectedItemPosition) {
|
||||||
|
0 -> currentList.sortBy { it.Name?.lowercase() ?: "" }
|
||||||
|
1 -> currentList.sortByDescending { it.from == "SMI" } // Changed to sortByDescending
|
||||||
|
2 -> currentList.sortByDescending { it.from == "IISc" } // Changed to sortByDescending
|
||||||
|
}
|
||||||
|
employeeAdapter.submitList(currentList)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupRecyclerView() {
|
||||||
|
employeeAdapter = EmployeeQrAdapter(
|
||||||
|
onShareClick = { employee -> shareQRCode(employee) },
|
||||||
|
onItemClick = { employee -> showQRCode(employee) }
|
||||||
|
)
|
||||||
|
binding.recyclerViewEmployees.apply {
|
||||||
|
adapter = employeeAdapter
|
||||||
|
layoutManager = LinearLayoutManager(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fetchEmployees() {
|
||||||
|
db.collection("employees")
|
||||||
|
.addSnapshotListener { snapshot, e ->
|
||||||
|
if (e != null) {
|
||||||
|
// Handle error
|
||||||
|
Toast.makeText(context, "Error fetching employees: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||||
|
return@addSnapshotListener
|
||||||
|
}
|
||||||
|
|
||||||
|
employeeList = snapshot?.documents?.mapNotNull { doc ->
|
||||||
|
val employee = doc.toObject(Employee::class.java)
|
||||||
|
employee?.apply {
|
||||||
|
Id = doc.id
|
||||||
|
Name = doc.getString("name") ?: doc.getString("Name") ?: this.Name
|
||||||
|
from = doc.getString("from") ?: ""
|
||||||
|
}
|
||||||
|
} ?: emptyList()
|
||||||
|
|
||||||
|
// Sorting manually to handle both "name" and "Name"
|
||||||
|
val sortedList = employeeList.sortedBy { it.Name?.lowercase() }
|
||||||
|
|
||||||
|
employeeAdapter.submitList(sortedList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateQRCode(employeeId: String): Bitmap {
|
||||||
|
val qrCodeWriter = QRCodeWriter()
|
||||||
|
val bitMatrix = qrCodeWriter.encode(employeeId, BarcodeFormat.QR_CODE, 512, 512)
|
||||||
|
val width = bitMatrix.width
|
||||||
|
val height = bitMatrix.height
|
||||||
|
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565)
|
||||||
|
|
||||||
|
for (x in 0 until width) {
|
||||||
|
for (y in 0 until height) {
|
||||||
|
bitmap.setPixel(x, y, if (bitMatrix[x, y]) Color.BLACK else Color.WHITE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bitmap
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showQRCode(employee: Employee) {
|
||||||
|
val qrBitmap = generateQRCode(employee.Id)
|
||||||
|
val dialog = MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle("${employee.Name}'s QR Code")
|
||||||
|
.setView(ImageView(context).apply {
|
||||||
|
setImageBitmap(qrBitmap)
|
||||||
|
layoutParams = ViewGroup.LayoutParams(512, 512)
|
||||||
|
})
|
||||||
|
.setPositiveButton("Close") { dialog, _ -> dialog.dismiss() }
|
||||||
|
.create()
|
||||||
|
dialog.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun shareQRCode(employee: Employee) {
|
||||||
|
val qrBitmap = generateQRCode(employee.Id)
|
||||||
|
val shareIntent = Intent(Intent.ACTION_SEND).apply {
|
||||||
|
type = "image/*"
|
||||||
|
putExtra(Intent.EXTRA_STREAM, saveQRCodeToCache(qrBitmap, employee.Id))
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
startActivity(Intent.createChooser(shareIntent, "Share QR Code"))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveQRCodeToCache(bitmap: Bitmap, employeeId: String): Uri {
|
||||||
|
val file = File(requireContext().cacheDir, "qr_${employeeId}.png")
|
||||||
|
FileOutputStream(file).use { stream ->
|
||||||
|
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
|
||||||
|
}
|
||||||
|
return FileProvider.getUriForFile(
|
||||||
|
requireContext(),
|
||||||
|
"${requireContext().packageName}.provider",
|
||||||
|
file
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package `in`.sminnovations.smiadmin.ui.fragments.attendance
|
|||||||
|
|
||||||
import android.R
|
import android.R
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
@@ -24,6 +25,9 @@ class AttendanceDetailFragment : Fragment() {
|
|||||||
private val months = arrayOf("January", "February", "March", "April", "May", "June",
|
private val months = arrayOf("January", "February", "March", "April", "May", "June",
|
||||||
"July", "August", "September", "October", "November", "December")
|
"July", "August", "September", "October", "November", "December")
|
||||||
|
|
||||||
|
// Add years (you might want to adjust the range based on your needs)
|
||||||
|
private val years = (2024..2030).toList()
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
@@ -41,11 +45,13 @@ class AttendanceDetailFragment : Fragment() {
|
|||||||
|
|
||||||
binding.textViewEmployeeName.text = employeeName
|
binding.textViewEmployeeName.text = employeeName
|
||||||
setupMonthSpinner()
|
setupMonthSpinner()
|
||||||
|
setupYearSpinner()
|
||||||
setupRecyclerView()
|
setupRecyclerView()
|
||||||
|
|
||||||
// Get current month (1-12)
|
// Get current month (1-12) and year
|
||||||
val currentMonth = Calendar.getInstance().get(Calendar.MONTH) + 1
|
val currentMonth = Calendar.getInstance().get(Calendar.MONTH) + 1
|
||||||
fetchAttendanceData(employeeId, currentMonth)
|
val currentYear = Calendar.getInstance().get(Calendar.YEAR)
|
||||||
|
fetchAttendanceData(employeeId, currentMonth, currentYear)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupMonthSpinner() {
|
private fun setupMonthSpinner() {
|
||||||
@@ -63,8 +69,35 @@ class AttendanceDetailFragment : Fragment() {
|
|||||||
binding.spinnerMonth.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
binding.spinnerMonth.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||||
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||||
val employeeId = arguments?.getString("employeeId") ?: return
|
val employeeId = arguments?.getString("employeeId") ?: return
|
||||||
// position + 1 because months in Calendar are 0-based
|
val selectedYear = years[binding.spinnerYear.selectedItemPosition]
|
||||||
fetchAttendanceData(employeeId, position + 1)
|
fetchAttendanceData(employeeId, position + 1, selectedYear)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNothingSelected(parent: AdapterView<*>?) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupYearSpinner() {
|
||||||
|
val adapter = ArrayAdapter(
|
||||||
|
requireContext(),
|
||||||
|
R.layout.simple_spinner_item,
|
||||||
|
years
|
||||||
|
)
|
||||||
|
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||||
|
|
||||||
|
binding.spinnerYear.adapter = adapter
|
||||||
|
// Set current year as default
|
||||||
|
val currentYear = Calendar.getInstance().get(Calendar.YEAR)
|
||||||
|
val currentYearPosition = years.indexOf(currentYear)
|
||||||
|
if (currentYearPosition != -1) {
|
||||||
|
binding.spinnerYear.setSelection(currentYearPosition)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.spinnerYear.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||||
|
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||||
|
val employeeId = arguments?.getString("employeeId") ?: return
|
||||||
|
val selectedMonth = binding.spinnerMonth.selectedItemPosition + 1
|
||||||
|
fetchAttendanceData(employeeId, selectedMonth, years[position])
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNothingSelected(parent: AdapterView<*>?) {}
|
override fun onNothingSelected(parent: AdapterView<*>?) {}
|
||||||
@@ -76,7 +109,7 @@ class AttendanceDetailFragment : Fragment() {
|
|||||||
binding.recyclerViewAttendance.adapter = adapter
|
binding.recyclerViewAttendance.adapter = adapter
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchAttendanceData(employeeId: String, month: Int) {
|
private fun fetchAttendanceData(employeeId: String, month: Int, year: Int) {
|
||||||
binding.progressBar.visibility = View.VISIBLE
|
binding.progressBar.visibility = View.VISIBLE
|
||||||
|
|
||||||
db.collection("attendance_$employeeId")
|
db.collection("attendance_$employeeId")
|
||||||
@@ -87,16 +120,42 @@ class AttendanceDetailFragment : Fragment() {
|
|||||||
try {
|
try {
|
||||||
val dateParts = doc.id.split("-")
|
val dateParts = doc.id.split("-")
|
||||||
val docMonth = dateParts[1].toInt()
|
val docMonth = dateParts[1].toInt()
|
||||||
|
val docYear = dateParts[2].toInt()
|
||||||
|
|
||||||
// Only include records for selected month
|
// Only include records for selected month and year
|
||||||
if (docMonth == month) {
|
if (docMonth == month && docYear == year) {
|
||||||
AttendanceData(
|
// Get all fields from the document
|
||||||
date = doc.id,
|
val data = doc.data ?: return@mapNotNull null
|
||||||
checkIn = doc.getString("checkIn") ?: "N/A",
|
|
||||||
checkOut = doc.getString("checkOut") ?: "N/A"
|
// Create maps to store check-ins and check-outs
|
||||||
)
|
val checkIns = mutableMapOf<String, String>()
|
||||||
|
val checkOuts = mutableMapOf<String, String>()
|
||||||
|
|
||||||
|
// Iterate through all fields in the document
|
||||||
|
data.forEach { (key, value) ->
|
||||||
|
when {
|
||||||
|
// Match checkIn1, checkIn2, etc.
|
||||||
|
key.startsWith("checkIn") -> {
|
||||||
|
checkIns[key] = (value as? String) ?: "N/A"
|
||||||
|
}
|
||||||
|
// Match checkOut1, checkOut2, etc.
|
||||||
|
key.startsWith("checkOut") -> {
|
||||||
|
checkOuts[key] = (value as? String) ?: "N/A"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only create AttendanceData if there's at least one check-in
|
||||||
|
if (checkIns.isNotEmpty()) {
|
||||||
|
AttendanceData(
|
||||||
|
date = doc.id,
|
||||||
|
checkIns = checkIns,
|
||||||
|
checkOuts = checkOuts
|
||||||
|
)
|
||||||
|
} else null
|
||||||
} else null
|
} else null
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
Log.e("AttendanceMapping", "Error mapping document: ${doc.id}", e)
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class EmployeeAttendanceListFragment : Fragment() {
|
|||||||
|
|
||||||
private lateinit var adapter: EmployeeStatusListAdapter
|
private lateinit var adapter: EmployeeStatusListAdapter
|
||||||
private val db = FirebaseFirestore.getInstance()
|
private val db = FirebaseFirestore.getInstance()
|
||||||
|
private var allEmployees = listOf<Employee>() // Store all employees
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
@@ -33,9 +34,35 @@ class EmployeeAttendanceListFragment : Fragment() {
|
|||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
setupRecyclerView()
|
setupRecyclerView()
|
||||||
|
setupSearchView()
|
||||||
fetchEmployees()
|
fetchEmployees()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setupSearchView() {
|
||||||
|
binding.searchView.setOnQueryTextListener(object : androidx.appcompat.widget.SearchView.OnQueryTextListener {
|
||||||
|
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onQueryTextChange(newText: String?): Boolean {
|
||||||
|
filterEmployees(newText)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun filterEmployees(query: String?) {
|
||||||
|
if (query.isNullOrBlank()) {
|
||||||
|
adapter.updateEmployees(allEmployees)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val filteredList = allEmployees.filter {
|
||||||
|
it.Name.contains(query, ignoreCase = true)
|
||||||
|
}
|
||||||
|
adapter.updateEmployees(filteredList)
|
||||||
|
}
|
||||||
|
|
||||||
private fun setupRecyclerView() {
|
private fun setupRecyclerView() {
|
||||||
adapter = EmployeeStatusListAdapter { employee ->
|
adapter = EmployeeStatusListAdapter { employee ->
|
||||||
val bundle = Bundle().apply {
|
val bundle = Bundle().apply {
|
||||||
@@ -57,13 +84,19 @@ class EmployeeAttendanceListFragment : Fragment() {
|
|||||||
.get()
|
.get()
|
||||||
.addOnSuccessListener { snapshot ->
|
.addOnSuccessListener { snapshot ->
|
||||||
binding.progressBar.visibility = View.GONE
|
binding.progressBar.visibility = View.GONE
|
||||||
val employees = snapshot.documents.map { doc ->
|
allEmployees = snapshot.documents.mapNotNull { doc ->
|
||||||
Employee(
|
val status = doc.getBoolean("EmployeeStatus") ?: true // Default to true if field is missing
|
||||||
Id = doc.getString("Id") ?: "Unknown",
|
if (status) {
|
||||||
Name = doc.getString("Name") ?: "Unknown"
|
Employee(
|
||||||
)
|
Id = doc.getString("Id") ?: doc.getString("id") ?: "",
|
||||||
|
Name = doc.getString("Name") ?: doc.getString("name") ?: "",
|
||||||
|
from = doc.getString("from") ?: ""
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
null // Exclude employees with EmployeeStatus == false
|
||||||
|
}
|
||||||
}.sortedBy { it.Name }
|
}.sortedBy { it.Name }
|
||||||
adapter.updateEmployees(employees)
|
adapter.updateEmployees(allEmployees)
|
||||||
}
|
}
|
||||||
.addOnFailureListener { e ->
|
.addOnFailureListener { e ->
|
||||||
binding.progressBar.visibility = View.GONE
|
binding.progressBar.visibility = View.GONE
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
package `in`.sminnovations.smiadmin.ui.fragments.attendance
|
package `in`.sminnovations.smiadmin.ui.fragments.attendance
|
||||||
|
|
||||||
|
import android.R
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.AdapterView
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import com.google.firebase.firestore.FirebaseFirestore
|
import com.google.firebase.firestore.FirebaseFirestore
|
||||||
@@ -15,7 +18,7 @@ import `in`.sminnovations.smiadmin.databinding.FragmentEmployeeListBinding
|
|||||||
class EmployeeListFragment : Fragment() {
|
class EmployeeListFragment : Fragment() {
|
||||||
private var _binding: FragmentEmployeeListBinding? = null
|
private var _binding: FragmentEmployeeListBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
private var employeeList = emptyList<Employee>()
|
||||||
private lateinit var adapter: EmployeeListAdapter
|
private lateinit var adapter: EmployeeListAdapter
|
||||||
private val db = FirebaseFirestore.getInstance()
|
private val db = FirebaseFirestore.getInstance()
|
||||||
|
|
||||||
@@ -30,11 +33,58 @@ class EmployeeListFragment : Fragment() {
|
|||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
setupSpinner()
|
||||||
setupRecyclerView()
|
setupRecyclerView()
|
||||||
|
setupSearchView()
|
||||||
fetchEmployees()
|
fetchEmployees()
|
||||||
}
|
}
|
||||||
|
private fun setupSearchView() {
|
||||||
|
binding.searchView.setOnQueryTextListener(object : androidx.appcompat.widget.SearchView.OnQueryTextListener {
|
||||||
|
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onQueryTextChange(newText: String?): Boolean {
|
||||||
|
filterEmployees(newText)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun filterEmployees(query: String?) {
|
||||||
|
if (query.isNullOrBlank()) {
|
||||||
|
adapter.updateEmployees(employeeList)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val filteredList = employeeList.filter {
|
||||||
|
it.Name.contains(query, ignoreCase = true)
|
||||||
|
}
|
||||||
|
adapter.updateEmployees(filteredList)
|
||||||
|
}
|
||||||
|
private fun setupSpinner() {
|
||||||
|
val sortOptions = arrayOf("Name (A-Z)", "SMI First", "IISc First")
|
||||||
|
val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, sortOptions)
|
||||||
|
adapter.setDropDownViewResource(R.layout.simple_spinner_dropdown_item)
|
||||||
|
|
||||||
|
binding.spinnerSort.adapter = adapter
|
||||||
|
binding.spinnerSort.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||||
|
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||||
|
sortEmployees()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNothingSelected(parent: AdapterView<*>?) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private fun sortEmployees() {
|
||||||
|
val currentList = adapter.employees.toMutableList()
|
||||||
|
when (binding.spinnerSort.selectedItemPosition) {
|
||||||
|
0 -> currentList.sortBy { it.Name?.lowercase() ?: "" }
|
||||||
|
1 -> currentList.sortByDescending { it.from == "SMI" } // Changed to sortByDescending
|
||||||
|
2 -> currentList.sortByDescending { it.from == "IISc" } // Changed to sortByDescending
|
||||||
|
}
|
||||||
|
adapter.updateEmployees(currentList)
|
||||||
|
}
|
||||||
private fun setupRecyclerView() {
|
private fun setupRecyclerView() {
|
||||||
adapter = EmployeeListAdapter { employee, newStatus ->
|
adapter = EmployeeListAdapter { employee, newStatus ->
|
||||||
updateEmployeeStatus(employee, newStatus)
|
updateEmployeeStatus(employee, newStatus)
|
||||||
@@ -56,13 +106,14 @@ class EmployeeListFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
snapshot?.let {
|
snapshot?.let {
|
||||||
val employeeList = snapshot.documents.map { doc ->
|
employeeList = snapshot.documents.map { doc ->
|
||||||
Employee(
|
Employee(
|
||||||
Id = doc.getString("Id") ?: "",
|
Id = doc.getString("Id") ?: doc.getString("id") ?: "",
|
||||||
Name = doc.getString("Name") ?: "",
|
Name = doc.getString("Name") ?: doc.getString("name") ?: "",
|
||||||
EmployeeStatus = doc.getBoolean("EmployeeStatus") ?: false
|
EmployeeStatus = doc.getBoolean("EmployeeStatus") ?: false,
|
||||||
|
from = doc.getString("from") ?: ""
|
||||||
)
|
)
|
||||||
}
|
}.sortedBy { it.Name?.lowercase() }
|
||||||
adapter.updateEmployees(employeeList)
|
adapter.updateEmployees(employeeList)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package `in`.sminnovations.smiadmin.ui.utils
|
||||||
|
|
||||||
|
import android.app.AlertDialog
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.widget.CalendarView
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import `in`.sminnovations.smiadmin.R
|
||||||
|
import java.util.Calendar
|
||||||
|
class DailyMonthYearPickerDialog : DialogFragment() {
|
||||||
|
private var onDateSelected: ((Calendar) -> Unit)? = null
|
||||||
|
private var selectedDate: Calendar = Calendar.getInstance()
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
// Inflate the custom layout
|
||||||
|
val view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_date_month_year_picker, null)
|
||||||
|
|
||||||
|
// Initialize CalendarView
|
||||||
|
val calendarPicker = view.findViewById<CalendarView>(R.id.calendarViewPicker)
|
||||||
|
|
||||||
|
// Set default date
|
||||||
|
calendarPicker.date = selectedDate.timeInMillis
|
||||||
|
|
||||||
|
// Listen for date changes
|
||||||
|
calendarPicker.setOnDateChangeListener { _, year, month, dayOfMonth ->
|
||||||
|
selectedDate.set(Calendar.YEAR, year)
|
||||||
|
selectedDate.set(Calendar.MONTH, month)
|
||||||
|
selectedDate.set(Calendar.DAY_OF_MONTH, dayOfMonth)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the Dialog
|
||||||
|
val dialog = AlertDialog.Builder(requireContext())
|
||||||
|
.setView(view)
|
||||||
|
.setTitle("Select Date for the Report")
|
||||||
|
.setPositiveButton("Generate Report") { _, _ ->
|
||||||
|
// Pass the selected date back through the callback
|
||||||
|
onDateSelected?.invoke(selectedDate)
|
||||||
|
}
|
||||||
|
.setNegativeButton("Cancel", null)
|
||||||
|
.create()
|
||||||
|
|
||||||
|
return dialog
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setOnDateSelectedListener(listener: (Calendar) -> Unit) {
|
||||||
|
onDateSelected = listener
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,20 @@
|
|||||||
package `in`.sminnovations.smiadmin.ui.utils
|
package `in`.sminnovations.smiadmin.ui.utils
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.app.ProgressDialog
|
import android.app.ProgressDialog
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Environment
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.google.firebase.Firebase
|
import com.google.firebase.Firebase
|
||||||
|
import com.google.firebase.firestore.Source
|
||||||
import com.google.firebase.firestore.firestore
|
import com.google.firebase.firestore.firestore
|
||||||
import `in`.sminnovations.smiadmin.data.Constant
|
import `in`.sminnovations.smiadmin.data.Constant
|
||||||
import `in`.sminnovations.smiadmin.data.Employee
|
import `in`.sminnovations.smiadmin.data.Employee
|
||||||
@@ -13,16 +23,38 @@ import kotlinx.coroutines.Job
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.tasks.await
|
import kotlinx.coroutines.tasks.await
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
class ExportTask(private val fragment: Fragment) {
|
class ExportTask(private val fragment: Fragment) {
|
||||||
private val db = Firebase.firestore
|
private val db = Firebase.firestore
|
||||||
private var currentExcelTask: Job? = null
|
private var currentExcelTask: Job? = null
|
||||||
|
|
||||||
fun exportAttendance(year: Int, month: Int) {
|
// Add permission check
|
||||||
|
private fun checkStoragePermission(): Boolean {
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
true // Android 10 and above don't need explicit storage permission for Downloads
|
||||||
|
} else {
|
||||||
|
ContextCompat.checkSelfPermission(
|
||||||
|
fragment.requireContext(),
|
||||||
|
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||||
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun exportAttendance(year: Int, month: Int, source: String) {
|
||||||
|
if (!checkStoragePermission()) {
|
||||||
|
ActivityCompat.requestPermissions(
|
||||||
|
fragment.requireActivity(),
|
||||||
|
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
|
||||||
|
STORAGE_PERMISSION_CODE
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Cancel any existing task
|
// Cancel any existing task
|
||||||
currentExcelTask?.cancel()
|
currentExcelTask?.cancel()
|
||||||
|
|
||||||
// Show progress dialog
|
|
||||||
val progressDialog = ProgressDialog(fragment.requireContext()).apply {
|
val progressDialog = ProgressDialog(fragment.requireContext()).apply {
|
||||||
setMessage("Generating attendance report...")
|
setMessage("Generating attendance report...")
|
||||||
setCancelable(false)
|
setCancelable(false)
|
||||||
@@ -31,34 +63,99 @@ class ExportTask(private val fragment: Fragment) {
|
|||||||
|
|
||||||
currentExcelTask = fragment.lifecycleScope.launch(Dispatchers.Main) {
|
currentExcelTask = fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
try {
|
try {
|
||||||
|
val company = if (source == "SMI"){
|
||||||
|
Constant.COMPANY_NAME
|
||||||
|
}else{
|
||||||
|
"IISc-OMI_LAB"
|
||||||
|
}
|
||||||
// First get all employees
|
// First get all employees
|
||||||
val employeesSnapshot = withContext(Dispatchers.IO) {
|
val employeesSnapshot = withContext(Dispatchers.IO) {
|
||||||
db.collection("employees")
|
db.collection("employees")
|
||||||
.whereEqualTo("EmployeeStatus", true)
|
.whereEqualTo("EmployeeStatus", true)
|
||||||
|
.whereEqualTo("from",source)
|
||||||
.get()
|
.get()
|
||||||
.await()
|
.await()
|
||||||
}
|
}
|
||||||
|
|
||||||
val employees = employeesSnapshot.documents.map { doc ->
|
val employees = employeesSnapshot.documents.map { doc ->
|
||||||
Employee(
|
Employee(
|
||||||
Id = doc.getString("Id") ?: "Unknown",
|
Id = doc.getString("Id") ?: doc.getString("id") ?: "",
|
||||||
Name = doc.getString("Name") ?: "Unknown"
|
Name = doc.getString("Name") ?: doc.getString("name") ?: "",
|
||||||
)
|
)
|
||||||
}.sortedBy { it.Name }
|
}.sortedBy { it.Name }
|
||||||
|
|
||||||
// Switch to IO dispatcher for file operations
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val pdfGenerator = PDFGenerator(fragment.requireContext(), Constant.COMPANY_NAME)
|
val pdfGenerator = PDFGenerator(fragment.requireContext(), company)
|
||||||
pdfGenerator.generateAttendanceReport(employees, year, month)
|
pdfGenerator.generateAttendanceReport(employees, year, month,source)
|
||||||
}.also { pdfFile ->
|
}.also { pdfFile ->
|
||||||
// Switch back to Main dispatcher for UI updates
|
withContext(Dispatchers.IO) {
|
||||||
withContext(Dispatchers.Main) {
|
try {
|
||||||
progressDialog.dismiss()
|
// Verify file exists and is readable
|
||||||
Toast.makeText(
|
if (!pdfFile.exists() || !pdfFile.canRead()) {
|
||||||
fragment.context,
|
throw IOException("Generated PDF file is not accessible")
|
||||||
"Report saved in Downloads folder: ${pdfFile.name}",
|
}
|
||||||
Toast.LENGTH_LONG
|
|
||||||
).show()
|
val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
||||||
|
val pdfFileInDownloads = File(downloadsDir, pdfFile.name)
|
||||||
|
|
||||||
|
// Copy file to Downloads if needed
|
||||||
|
if (!pdfFileInDownloads.exists()) {
|
||||||
|
pdfFile.copyTo(pdfFileInDownloads, overwrite = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the file was copied successfully
|
||||||
|
if (!pdfFileInDownloads.exists() || !pdfFileInDownloads.canRead()) {
|
||||||
|
throw IOException("Failed to copy PDF to Downloads folder")
|
||||||
|
}
|
||||||
|
|
||||||
|
val emailIntent = Intent(Intent.ACTION_SEND).apply {
|
||||||
|
type = "application/pdf"
|
||||||
|
putExtra(Intent.EXTRA_EMAIL, arrayOf("admin@sminnovations.in"))
|
||||||
|
putExtra(Intent.EXTRA_SUBJECT, "Attendance Report - $month $year")
|
||||||
|
putExtra(Intent.EXTRA_TEXT, "Please find attached the attendance report.")
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Convert file URI to content URI for sharing
|
||||||
|
val contentUri = FileProvider.getUriForFile(
|
||||||
|
fragment.requireContext(),
|
||||||
|
"${fragment.requireContext().packageName}.provider",
|
||||||
|
pdfFileInDownloads
|
||||||
|
)
|
||||||
|
putExtra(Intent.EXTRA_STREAM, contentUri)
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
throw IOException("FileProvider not properly configured: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch to Main dispatcher for starting the email activity
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
progressDialog.dismiss()
|
||||||
|
try {
|
||||||
|
fragment.startActivity(Intent.createChooser(emailIntent, "Send Email"))
|
||||||
|
Toast.makeText(
|
||||||
|
fragment.context,
|
||||||
|
"Report saved in Downloads folder: ${pdfFile.name}",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
} catch (e: ActivityNotFoundException) {
|
||||||
|
Toast.makeText(
|
||||||
|
fragment.context,
|
||||||
|
"No email app found",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
progressDialog.dismiss()
|
||||||
|
Toast.makeText(
|
||||||
|
fragment.context,
|
||||||
|
"Error sending email: ${e.message}",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -78,4 +175,8 @@ class ExportTask(private val fragment: Fragment) {
|
|||||||
fun cleanup() {
|
fun cleanup() {
|
||||||
currentExcelTask?.cancel()
|
currentExcelTask?.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val STORAGE_PERMISSION_CODE = 101
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package `in`.sminnovations.smiadmin.ui.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
|
import android.os.Build
|
||||||
|
|
||||||
|
class NetworkUtils(private val context: Context) {
|
||||||
|
fun isInternetAvailable(): Boolean {
|
||||||
|
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
val network = connectivityManager.activeNetwork ?: return false
|
||||||
|
val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false
|
||||||
|
return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||||
|
} else {
|
||||||
|
val networkInfo = connectivityManager.activeNetworkInfo ?: return false
|
||||||
|
return networkInfo.isConnected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,523 @@
|
|||||||
|
package `in`.sminnovations.smiadmin.ui.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.LinearGradient
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.Path
|
||||||
|
import android.graphics.Shader
|
||||||
|
import android.graphics.Typeface
|
||||||
|
import android.graphics.pdf.PdfDocument
|
||||||
|
import android.os.Environment
|
||||||
|
import com.google.firebase.Firebase
|
||||||
|
import com.google.firebase.firestore.firestore
|
||||||
|
import `in`.sminnovations.smiadmin.data.Employee
|
||||||
|
import kotlinx.coroutines.tasks.await
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
|
||||||
|
class PDFGenerator_daily(
|
||||||
|
private val context: Context,
|
||||||
|
private val companyName: String
|
||||||
|
){
|
||||||
|
private lateinit var document: PdfDocument
|
||||||
|
private lateinit var currentPage: PdfDocument.Page
|
||||||
|
private lateinit var canvas: android.graphics.Canvas
|
||||||
|
private var currentY: Float = 0f
|
||||||
|
|
||||||
|
// Constants for page layout
|
||||||
|
private val pageWidth = 595f // A4 width
|
||||||
|
private val pageHeight = 842f // A4 height
|
||||||
|
private val margin = 10f
|
||||||
|
private val contentWidth = pageWidth - (2 * margin)
|
||||||
|
|
||||||
|
// Professional color scheme
|
||||||
|
private val primaryColor = Color.rgb(41, 128, 185) // Professional blue
|
||||||
|
private val secondaryColor = Color.rgb(44, 62, 80) // Dark blue
|
||||||
|
private val accentColor = Color.rgb(230, 126, 34) // Orange for highlights
|
||||||
|
private val backgroundColor = Color.rgb(236, 240, 241) // Light gray
|
||||||
|
private val borderColor = Color.rgb(189, 195, 199) // Medium gray
|
||||||
|
private val textColor = Color.rgb(52, 73, 94) // Dark gray for text
|
||||||
|
|
||||||
|
private var pageNumber = 1
|
||||||
|
|
||||||
|
// Data class for attendance record
|
||||||
|
data class AttendanceRecord(
|
||||||
|
val checkInTimes: List<String>,
|
||||||
|
val checkOutTimes: List<String>,
|
||||||
|
val isCompleted: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun generateDailyReport(
|
||||||
|
employees: List<Employee>,
|
||||||
|
selectedDate: Calendar,
|
||||||
|
source: String
|
||||||
|
): File {
|
||||||
|
document = PdfDocument()
|
||||||
|
createNewPage()
|
||||||
|
|
||||||
|
// Create paint objects
|
||||||
|
val titlePaint = Paint().apply {
|
||||||
|
textSize = 32f
|
||||||
|
typeface = Typeface.create("sans-serif-light", Typeface.BOLD)
|
||||||
|
color = primaryColor
|
||||||
|
textAlign = Paint.Align.CENTER
|
||||||
|
}
|
||||||
|
|
||||||
|
val subtitlePaint = Paint().apply {
|
||||||
|
textSize = 24f
|
||||||
|
typeface = Typeface.create("sans-serif-light", Typeface.NORMAL)
|
||||||
|
color = secondaryColor
|
||||||
|
textAlign = Paint.Align.CENTER
|
||||||
|
}
|
||||||
|
|
||||||
|
val notePaint = Paint().apply {
|
||||||
|
textSize = 10f
|
||||||
|
typeface = Typeface.MONOSPACE
|
||||||
|
color = secondaryColor
|
||||||
|
textAlign = Paint.Align.CENTER
|
||||||
|
}
|
||||||
|
|
||||||
|
val tableHeaderPaint = Paint().apply {
|
||||||
|
textSize = 14f
|
||||||
|
typeface = Typeface.create("sans-serif-medium", Typeface.BOLD)
|
||||||
|
color = Color.WHITE
|
||||||
|
}
|
||||||
|
|
||||||
|
val regularPaint = Paint().apply {
|
||||||
|
textSize = 12f
|
||||||
|
typeface = Typeface.create("sans-serif", Typeface.NORMAL)
|
||||||
|
color = textColor
|
||||||
|
}
|
||||||
|
|
||||||
|
val highlightPaint = Paint().apply {
|
||||||
|
textSize = 12f
|
||||||
|
typeface = Typeface.create("sans-serif-medium", Typeface.BOLD)
|
||||||
|
color = accentColor
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format date for display and Firebase query
|
||||||
|
val dateFormat = SimpleDateFormat("dd MMMM yyyy", Locale.getDefault())
|
||||||
|
val formattedDate = dateFormat.format(selectedDate.time)
|
||||||
|
|
||||||
|
val yearStr = selectedDate.get(Calendar.YEAR).toString()
|
||||||
|
val monthStr = String.format("%02d", selectedDate.get(Calendar.MONTH) + 1)
|
||||||
|
val dayStr = String.format("%02d", selectedDate.get(Calendar.DAY_OF_MONTH))
|
||||||
|
val firebaseDateKey = "$dayStr-$monthStr-$yearStr"
|
||||||
|
|
||||||
|
//
|
||||||
|
drawTopDisclaimer(notePaint)
|
||||||
|
// Draw report header
|
||||||
|
drawReportHeader(formattedDate, titlePaint, subtitlePaint)
|
||||||
|
|
||||||
|
// Draw table headers
|
||||||
|
drawTableHeaders(tableHeaderPaint)
|
||||||
|
|
||||||
|
// Process each employee
|
||||||
|
employees.forEach { employee ->
|
||||||
|
if (currentY > pageHeight - 100) {
|
||||||
|
finishCurrentPage()
|
||||||
|
createNewPage()
|
||||||
|
drawTableHeaders(tableHeaderPaint)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get attendance data for this employee for the selected date
|
||||||
|
val attendanceDoc = Firebase.firestore
|
||||||
|
.collection("attendance_${employee.Id}")
|
||||||
|
.document(firebaseDateKey)
|
||||||
|
.get()
|
||||||
|
.await()
|
||||||
|
|
||||||
|
if (attendanceDoc.exists()) {
|
||||||
|
// Extract check-in and check-out times
|
||||||
|
val checkInTimes = mutableListOf<String>()
|
||||||
|
val checkOutTimes = mutableListOf<String>()
|
||||||
|
val isCompleted = attendanceDoc.getBoolean("DayCompleted") ?: false
|
||||||
|
|
||||||
|
// Get all check-in times
|
||||||
|
var i = 1
|
||||||
|
while (true) {
|
||||||
|
val checkInKey = "checkIn$i"
|
||||||
|
val checkInTime = attendanceDoc.getString(checkInKey)
|
||||||
|
if (checkInTime != null) {
|
||||||
|
checkInTimes.add(checkInTime)
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all check-out times
|
||||||
|
i = 1
|
||||||
|
while (true) {
|
||||||
|
val checkOutKey = "checkOut$i"
|
||||||
|
val checkOutTime = attendanceDoc.getString(checkOutKey)
|
||||||
|
if (checkOutTime != null) {
|
||||||
|
checkOutTimes.add(checkOutTime)
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val attendanceRecord = AttendanceRecord(
|
||||||
|
checkInTimes = checkInTimes,
|
||||||
|
checkOutTimes = checkOutTimes,
|
||||||
|
isCompleted = isCompleted
|
||||||
|
)
|
||||||
|
|
||||||
|
drawEmployeeAttendanceRow(employee, attendanceRecord, regularPaint, highlightPaint)
|
||||||
|
} else {
|
||||||
|
// Employee was absent
|
||||||
|
drawEmployeeAbsentRow(employee, regularPaint, highlightPaint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add footer with page numbers
|
||||||
|
drawFooter(regularPaint)
|
||||||
|
|
||||||
|
// Finish and save document
|
||||||
|
finishCurrentPage()
|
||||||
|
return saveDocument(selectedDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawReportHeader(
|
||||||
|
formattedDate: String,
|
||||||
|
titlePaint: Paint,
|
||||||
|
subtitlePaint: Paint,
|
||||||
|
|
||||||
|
) {
|
||||||
|
// Draw decorative header background
|
||||||
|
val headerPath = Path().apply {
|
||||||
|
moveTo(0f, 0f)
|
||||||
|
lineTo(pageWidth, 0f)
|
||||||
|
lineTo(pageWidth, 120f)
|
||||||
|
quadTo(pageWidth/2, 140f, 0f, 120f)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.drawPath(headerPath, Paint().apply {
|
||||||
|
color = primaryColor
|
||||||
|
alpha = 20
|
||||||
|
})
|
||||||
|
|
||||||
|
// Draw company name
|
||||||
|
canvas.drawText(companyName, pageWidth/2, 70f, titlePaint)
|
||||||
|
|
||||||
|
// Draw report title
|
||||||
|
canvas.drawText(
|
||||||
|
"Daily Attendance Report",
|
||||||
|
pageWidth/2,
|
||||||
|
105f,
|
||||||
|
subtitlePaint
|
||||||
|
)
|
||||||
|
|
||||||
|
canvas.drawText(
|
||||||
|
formattedDate,
|
||||||
|
pageWidth/2,
|
||||||
|
135f,
|
||||||
|
subtitlePaint
|
||||||
|
)
|
||||||
|
currentY = 180f
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawTableHeaders(headerPaint: Paint) {
|
||||||
|
val headerHeight = 45f
|
||||||
|
|
||||||
|
// Create gradient for header background
|
||||||
|
val gradient = LinearGradient(
|
||||||
|
margin, currentY,
|
||||||
|
margin, currentY + headerHeight,
|
||||||
|
primaryColor,
|
||||||
|
secondaryColor,
|
||||||
|
Shader.TileMode.CLAMP
|
||||||
|
)
|
||||||
|
|
||||||
|
// Draw header background with gradient
|
||||||
|
canvas.drawRoundRect(
|
||||||
|
margin, currentY,
|
||||||
|
pageWidth - margin,
|
||||||
|
currentY + headerHeight,
|
||||||
|
8f, 8f,
|
||||||
|
Paint().apply { shader = gradient }
|
||||||
|
)
|
||||||
|
|
||||||
|
// Draw column headers
|
||||||
|
var xPos = margin + 15f
|
||||||
|
canvas.drawText("Employee Name", xPos, currentY + 30f, headerPaint)
|
||||||
|
|
||||||
|
xPos += 150f
|
||||||
|
canvas.drawText("Status", xPos, currentY + 30f, headerPaint)
|
||||||
|
|
||||||
|
xPos += 80f
|
||||||
|
canvas.drawText("Check-in", xPos, currentY + 30f, headerPaint)
|
||||||
|
|
||||||
|
xPos += 150f
|
||||||
|
canvas.drawText("Check-out", xPos, currentY + 30f, headerPaint)
|
||||||
|
|
||||||
|
xPos += 110f
|
||||||
|
canvas.drawText("Hours", xPos, currentY + 30f, headerPaint)
|
||||||
|
|
||||||
|
currentY += headerHeight + 5f
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawEmployeeAttendanceRow(
|
||||||
|
employee: Employee,
|
||||||
|
attendanceRecord: AttendanceRecord,
|
||||||
|
regularPaint: Paint,
|
||||||
|
highlightPaint: Paint
|
||||||
|
) {
|
||||||
|
// Calculate row height based on number of check-ins/outs
|
||||||
|
val entriesCount = maxOf(attendanceRecord.checkInTimes.size, attendanceRecord.checkOutTimes.size)
|
||||||
|
val rowHeight = 40f + (entriesCount * 20f)
|
||||||
|
|
||||||
|
// Draw row background
|
||||||
|
canvas.drawRoundRect(
|
||||||
|
margin, currentY,
|
||||||
|
pageWidth - margin,
|
||||||
|
currentY + rowHeight,
|
||||||
|
4f, 4f,
|
||||||
|
Paint().apply {
|
||||||
|
color = backgroundColor
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Draw employee name
|
||||||
|
canvas.drawText(
|
||||||
|
employee.Name,
|
||||||
|
margin + 15f,
|
||||||
|
currentY + 25f,
|
||||||
|
regularPaint
|
||||||
|
)
|
||||||
|
|
||||||
|
// Draw status
|
||||||
|
val status = if (attendanceRecord.isCompleted) "Present" else "Incomplete"
|
||||||
|
val statusPaint = if (attendanceRecord.isCompleted) {
|
||||||
|
Paint().apply {
|
||||||
|
textSize = 12f
|
||||||
|
typeface = Typeface.create("sans-serif-medium", Typeface.BOLD)
|
||||||
|
color = Color.rgb(46, 204, 113) // Green for present
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Paint().apply {
|
||||||
|
textSize = 12f
|
||||||
|
typeface = Typeface.create("sans-serif-medium", Typeface.BOLD)
|
||||||
|
color = Color.rgb(92, 75, 242) // Purple for incomplete
|
||||||
|
}
|
||||||
|
}
|
||||||
|
canvas.drawText(
|
||||||
|
status,
|
||||||
|
margin + 165f,
|
||||||
|
currentY + 25f,
|
||||||
|
statusPaint
|
||||||
|
)
|
||||||
|
|
||||||
|
// Draw check-in times
|
||||||
|
var checkInY = currentY + 25f
|
||||||
|
attendanceRecord.checkInTimes.forEachIndexed { index, time ->
|
||||||
|
// Extract only the time portion for display
|
||||||
|
val simplifiedTime = try {
|
||||||
|
val parsedTime = SimpleDateFormat("dd MMMM yyyy hh:mm:ss a", Locale.getDefault()).parse(time)
|
||||||
|
SimpleDateFormat("hh:mm:ss a", Locale.getDefault()).format(parsedTime)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
time // Fallback if parsing fails
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.drawText(
|
||||||
|
"$simplifiedTime",
|
||||||
|
margin + 245f,
|
||||||
|
checkInY,
|
||||||
|
regularPaint
|
||||||
|
)
|
||||||
|
checkInY += 20f
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw check-out times
|
||||||
|
var checkOutY = currentY + 25f
|
||||||
|
attendanceRecord.checkOutTimes.forEachIndexed { index, time ->
|
||||||
|
// Extract only the time portion for display
|
||||||
|
val simplifiedTime = try {
|
||||||
|
val parsedTime = SimpleDateFormat("dd MMMM yyyy hh:mm:ss a", Locale.getDefault()).parse(time)
|
||||||
|
SimpleDateFormat("hh:mm:ss a", Locale.getDefault()).format(parsedTime)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
time // Fallback if parsing fails
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.drawText(
|
||||||
|
"$simplifiedTime",
|
||||||
|
margin + 395f,
|
||||||
|
checkOutY,
|
||||||
|
regularPaint
|
||||||
|
)
|
||||||
|
checkOutY += 20f
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate and draw total hours
|
||||||
|
val totalHours = calculateTotalHours(attendanceRecord.checkInTimes, attendanceRecord.checkOutTimes)
|
||||||
|
canvas.drawText(
|
||||||
|
totalHours,
|
||||||
|
margin + 505f,
|
||||||
|
currentY + 25f,
|
||||||
|
highlightPaint
|
||||||
|
)
|
||||||
|
|
||||||
|
currentY += rowHeight + 10f
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawEmployeeAbsentRow(
|
||||||
|
employee: Employee,
|
||||||
|
regularPaint: Paint,
|
||||||
|
highlightPaint: Paint
|
||||||
|
) {
|
||||||
|
val rowHeight = 40f
|
||||||
|
|
||||||
|
// Draw row background with a slight red tint for absent
|
||||||
|
canvas.drawRoundRect(
|
||||||
|
margin, currentY,
|
||||||
|
pageWidth - margin,
|
||||||
|
currentY + rowHeight,
|
||||||
|
4f, 4f,
|
||||||
|
Paint().apply {
|
||||||
|
color = Color.rgb(245, 233, 233) // Light red tint
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Draw employee name
|
||||||
|
canvas.drawText(
|
||||||
|
employee.Name,
|
||||||
|
margin + 15f,
|
||||||
|
currentY + 25f,
|
||||||
|
regularPaint
|
||||||
|
)
|
||||||
|
|
||||||
|
// Draw absent status
|
||||||
|
val absentPaint = Paint().apply {
|
||||||
|
textSize = 12f
|
||||||
|
typeface = Typeface.create("sans-serif-medium", Typeface.BOLD)
|
||||||
|
color = Color.rgb(231, 76, 60) // Red for absent
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.drawText(
|
||||||
|
"Absent",
|
||||||
|
margin + 165f,
|
||||||
|
currentY + 25f,
|
||||||
|
absentPaint
|
||||||
|
)
|
||||||
|
|
||||||
|
// Draw dashes for check-in and check-out
|
||||||
|
canvas.drawText(
|
||||||
|
"—",
|
||||||
|
margin + 245f,
|
||||||
|
currentY + 25f,
|
||||||
|
regularPaint
|
||||||
|
)
|
||||||
|
|
||||||
|
canvas.drawText(
|
||||||
|
"—",
|
||||||
|
margin + 395f,
|
||||||
|
currentY + 25f,
|
||||||
|
regularPaint
|
||||||
|
)
|
||||||
|
|
||||||
|
// Draw zero hours
|
||||||
|
canvas.drawText(
|
||||||
|
"0.00",
|
||||||
|
margin + 505f,
|
||||||
|
currentY + 25f,
|
||||||
|
highlightPaint
|
||||||
|
)
|
||||||
|
currentY += rowHeight + 10f
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun calculateTotalHours(checkInTimes: List<String>, checkOutTimes: List<String>): String {
|
||||||
|
var totalMinutes = 0f
|
||||||
|
val dateFormat = SimpleDateFormat("dd MMMM yyyy hh:mm:ss a", Locale.getDefault())
|
||||||
|
|
||||||
|
// Calculate time for each check-in/check-out pair
|
||||||
|
for (i in 0 until minOf(checkInTimes.size, checkOutTimes.size)) {
|
||||||
|
try {
|
||||||
|
val checkInTime = dateFormat.parse(checkInTimes[i])
|
||||||
|
val checkOutTime = dateFormat.parse(checkOutTimes[i])
|
||||||
|
|
||||||
|
if (checkInTime != null && checkOutTime != null) {
|
||||||
|
// Calculate difference in minutes
|
||||||
|
val diffMillis = checkOutTime.time - checkInTime.time
|
||||||
|
val diffMinutes = diffMillis / (1000 * 60)
|
||||||
|
totalMinutes += diffMinutes
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Skip if there's an error parsing the date
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert total minutes to hours and minutes
|
||||||
|
val hours = totalMinutes.toInt() / 60
|
||||||
|
val minutes = totalMinutes.toInt() % 60
|
||||||
|
|
||||||
|
// Format as "HH:MM"
|
||||||
|
return String.format("%d:%02d", hours, minutes)
|
||||||
|
}
|
||||||
|
private fun drawFooter(paint: Paint, ) {
|
||||||
|
paint.textAlign = Paint.Align.CENTER
|
||||||
|
paint.textSize = 10f
|
||||||
|
|
||||||
|
canvas.drawText(
|
||||||
|
"Page ${pageNumber -1}",
|
||||||
|
pageWidth/2,
|
||||||
|
pageHeight - margin/2,
|
||||||
|
paint
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
private fun drawTopDisclaimer(notePaint: Paint){
|
||||||
|
|
||||||
|
val currentDate = SimpleDateFormat("dd MMMM yyyy HH:mm", Locale.getDefault())
|
||||||
|
.format(Calendar.getInstance().time)
|
||||||
|
|
||||||
|
canvas.drawText(
|
||||||
|
"This Report was generated by AdminApp on: $currentDate ",
|
||||||
|
pageWidth/2,
|
||||||
|
20f,
|
||||||
|
notePaint
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveDocument(selectedDate: Calendar): File {
|
||||||
|
val dateStr = SimpleDateFormat("yyyy_MM_dd", Locale.getDefault())
|
||||||
|
.format(selectedDate.time)
|
||||||
|
|
||||||
|
val fileName = "Daily_Attendance_Report_$dateStr.pdf"
|
||||||
|
val file = File(
|
||||||
|
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
||||||
|
fileName
|
||||||
|
)
|
||||||
|
|
||||||
|
FileOutputStream(file).use { outputStream ->
|
||||||
|
document.writeTo(outputStream)
|
||||||
|
}
|
||||||
|
|
||||||
|
document.close()
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createNewPage() {
|
||||||
|
val pageInfo = PdfDocument.PageInfo.Builder(
|
||||||
|
pageWidth.toInt(),
|
||||||
|
pageHeight.toInt(),
|
||||||
|
pageNumber++
|
||||||
|
).create()
|
||||||
|
|
||||||
|
currentPage = document.startPage(pageInfo)
|
||||||
|
canvas = currentPage.canvas
|
||||||
|
currentY = margin
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun finishCurrentPage() {
|
||||||
|
document.finishPage(currentPage)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +1,23 @@
|
|||||||
package `in`.sminnovations.smiadmin.ui.utils
|
package `in`.sminnovations.smiadmin.ui.utils
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.LinearGradient
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.Path
|
||||||
|
import android.graphics.Shader
|
||||||
|
import android.graphics.Typeface
|
||||||
import android.graphics.pdf.PdfDocument
|
import android.graphics.pdf.PdfDocument
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import java.io.File
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
import android.graphics.Paint
|
|
||||||
import android.graphics.Typeface
|
|
||||||
import android.graphics.Color
|
|
||||||
import com.google.firebase.Firebase
|
import com.google.firebase.Firebase
|
||||||
import com.google.firebase.firestore.firestore
|
import com.google.firebase.firestore.firestore
|
||||||
import `in`.sminnovations.smiadmin.data.Employee
|
import `in`.sminnovations.smiadmin.data.Employee
|
||||||
import kotlinx.coroutines.tasks.await
|
import kotlinx.coroutines.tasks.await
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.Calendar
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
class PDFGenerator(
|
class PDFGenerator(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
@@ -23,54 +27,69 @@ class PDFGenerator(
|
|||||||
private lateinit var currentPage: PdfDocument.Page
|
private lateinit var currentPage: PdfDocument.Page
|
||||||
private lateinit var canvas: android.graphics.Canvas
|
private lateinit var canvas: android.graphics.Canvas
|
||||||
private var currentY: Float = 0f
|
private var currentY: Float = 0f
|
||||||
private val startX = 40f
|
|
||||||
private val pageHeight = 842f // A4 height
|
|
||||||
private val pageWidth = 595f // A4 width
|
|
||||||
private var pageNumber = 1
|
|
||||||
|
|
||||||
// Define colors
|
// Constants for page layout
|
||||||
private val primaryColor = Color.rgb(41, 128, 185) // Blue
|
private val pageWidth = 595f // A4 width
|
||||||
private val secondaryColor = Color.rgb(44, 62, 80) // Dark Blue
|
private val pageHeight = 842f // A4 height
|
||||||
private val borderColor = Color.rgb(189, 195, 199) // Light Gray
|
private val margin = 40f
|
||||||
private val backgroundColor = Color.rgb(236, 240, 241) // Very Light Gray
|
private val contentWidth = pageWidth - (2 * margin)
|
||||||
|
|
||||||
|
// Professional color scheme
|
||||||
|
private val primaryColor = Color.rgb(41, 128, 185) // Professional blue
|
||||||
|
private val secondaryColor = Color.rgb(44, 62, 80) // Dark blue
|
||||||
|
private val accentColor = Color.rgb(230, 126, 34) // Orange for highlights
|
||||||
|
private val backgroundColor = Color.rgb(236, 240, 241) // Light gray
|
||||||
|
private val borderColor = Color.rgb(189, 195, 199) // Medium gray
|
||||||
|
private val textColor = Color.rgb(52, 73, 94) // Dark gray for text
|
||||||
|
|
||||||
|
private var pageNumber = 1
|
||||||
|
|
||||||
suspend fun generateAttendanceReport(
|
suspend fun generateAttendanceReport(
|
||||||
employees: List<Employee>,
|
employees: List<Employee>,
|
||||||
selectedYear: Int,
|
selectedYear: Int,
|
||||||
selectedMonth: Int
|
selectedMonth: Int,
|
||||||
|
source: String
|
||||||
): File {
|
): File {
|
||||||
document = PdfDocument()
|
document = PdfDocument()
|
||||||
createNewPage()
|
createNewPage()
|
||||||
|
|
||||||
// Setup paints
|
// Create paint objects
|
||||||
val headerPaint = Paint().apply {
|
val titlePaint = Paint().apply {
|
||||||
textSize = 28f
|
textSize = 32f
|
||||||
textAlign = Paint.Align.CENTER
|
typeface = Typeface.create("sans-serif-light", Typeface.BOLD)
|
||||||
typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
|
|
||||||
color = primaryColor
|
color = primaryColor
|
||||||
|
textAlign = Paint.Align.CENTER
|
||||||
}
|
}
|
||||||
|
|
||||||
val subHeaderPaint = Paint().apply {
|
val subtitlePaint = Paint().apply {
|
||||||
textSize = 20f
|
textSize = 24f
|
||||||
textAlign = Paint.Align.CENTER
|
typeface = Typeface.create("sans-serif-light", Typeface.NORMAL)
|
||||||
color = secondaryColor
|
color = secondaryColor
|
||||||
|
textAlign = Paint.Align.CENTER
|
||||||
}
|
}
|
||||||
|
|
||||||
val tableHeaderPaint = Paint().apply {
|
val tableHeaderPaint = Paint().apply {
|
||||||
textSize = 14f
|
textSize = 14f
|
||||||
textAlign = Paint.Align.LEFT
|
typeface = Typeface.create("sans-serif-medium", Typeface.BOLD)
|
||||||
typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
|
|
||||||
color = Color.WHITE
|
color = Color.WHITE
|
||||||
}
|
}
|
||||||
|
|
||||||
val tablePaint = Paint().apply {
|
val regularPaint = Paint().apply {
|
||||||
textSize = 12f
|
textSize = 12f
|
||||||
textAlign = Paint.Align.LEFT
|
typeface = Typeface.create("sans-serif", Typeface.NORMAL)
|
||||||
color = secondaryColor
|
color = textColor
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw headers and table
|
val highlightPaint = Paint().apply {
|
||||||
drawPageHeaders(headerPaint, subHeaderPaint, selectedYear, selectedMonth)
|
textSize = 12f
|
||||||
|
typeface = Typeface.create("sans-serif-medium", Typeface.BOLD)
|
||||||
|
color = accentColor
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw report header
|
||||||
|
drawReportHeader(selectedYear, selectedMonth, titlePaint, subtitlePaint)
|
||||||
|
|
||||||
|
// Draw table headers
|
||||||
drawTableHeaders(tableHeaderPaint)
|
drawTableHeaders(tableHeaderPaint)
|
||||||
|
|
||||||
// Process each employee
|
// Process each employee
|
||||||
@@ -78,7 +97,6 @@ class PDFGenerator(
|
|||||||
if (currentY > pageHeight - 100) {
|
if (currentY > pageHeight - 100) {
|
||||||
finishCurrentPage()
|
finishCurrentPage()
|
||||||
createNewPage()
|
createNewPage()
|
||||||
drawPageHeaders(headerPaint, subHeaderPaint, selectedYear, selectedMonth)
|
|
||||||
drawTableHeaders(tableHeaderPaint)
|
drawTableHeaders(tableHeaderPaint)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,58 +116,210 @@ class PDFGenerator(
|
|||||||
}
|
}
|
||||||
.sorted()
|
.sorted()
|
||||||
|
|
||||||
// Draw row background
|
// Calculate working days (excluding holidays based on company)
|
||||||
val rowPaint = Paint().apply {
|
val calendar = Calendar.getInstance().apply {
|
||||||
color = if ((employees.indexOf(employee) % 2) == 0) {
|
set(selectedYear, selectedMonth - 1, 1)
|
||||||
backgroundColor
|
}
|
||||||
} else {
|
val totalDays = calendar.getActualMaximum(Calendar.DAY_OF_MONTH)
|
||||||
Color.WHITE
|
|
||||||
|
// Count holidays based on company name
|
||||||
|
val holidays = (1..totalDays).count { day ->
|
||||||
|
calendar.set(Calendar.DAY_OF_MONTH, day)
|
||||||
|
when (companyName) {
|
||||||
|
"ShanMukha Innovations" -> calendar.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY ||
|
||||||
|
calendar.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY
|
||||||
|
"IISc-OMI_LAB" -> calendar.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY
|
||||||
|
else -> calendar.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY // default case
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val rowHeight = 60f
|
val workingDays = totalDays - holidays
|
||||||
canvas.drawRect(startX, currentY, pageWidth - startX, currentY + rowHeight, rowPaint)
|
|
||||||
|
|
||||||
// Draw employee details
|
// Calculate absent days (excluding holidays)
|
||||||
val nameY = currentY + 25f
|
val absentDays = (1..totalDays).count { day ->
|
||||||
canvas.drawText(employee.Name, startX + 10f, nameY, tablePaint)
|
calendar.set(Calendar.DAY_OF_MONTH, day)
|
||||||
canvas.drawText(
|
val isHoliday = when (companyName) {
|
||||||
"${presentDates.size} days",
|
"ShanMukha Innovations" -> calendar.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY ||
|
||||||
startX + 190f,
|
calendar.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY
|
||||||
nameY,
|
"IISc-OMI_LAB" -> calendar.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY
|
||||||
tablePaint
|
else -> calendar.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY
|
||||||
)
|
}
|
||||||
|
!isHoliday && !presentDates.contains(day)
|
||||||
// Format dates in groups of 5
|
|
||||||
val datesText = presentDates.chunked(5).joinToString("\n") { chunk ->
|
|
||||||
chunk.joinToString(", ") { day -> String.format("%02d", day) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw dates in multiple lines if needed
|
drawEmployeeRow(employee, presentDates, absentDays, workingDays, regularPaint, highlightPaint)
|
||||||
var dateY = nameY
|
|
||||||
datesText.split("\n").forEach { line ->
|
|
||||||
canvas.drawText(line, startX + 290f, dateY, tablePaint)
|
|
||||||
dateY += 15f
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw row borders
|
|
||||||
val borderPaint = Paint().apply {
|
|
||||||
style = Paint.Style.STROKE
|
|
||||||
strokeWidth = 0.5f
|
|
||||||
color = borderColor
|
|
||||||
}
|
|
||||||
canvas.drawLine(
|
|
||||||
startX, currentY + rowHeight,
|
|
||||||
pageWidth - startX, currentY + rowHeight,
|
|
||||||
borderPaint
|
|
||||||
)
|
|
||||||
|
|
||||||
currentY += rowHeight
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add footer with page numbers
|
||||||
|
drawFooter(regularPaint)
|
||||||
|
|
||||||
// Finish and save document
|
// Finish and save document
|
||||||
finishCurrentPage()
|
finishCurrentPage()
|
||||||
|
return saveDocument(selectedYear, selectedMonth)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawReportHeader(
|
||||||
|
selectedYear: Int,
|
||||||
|
selectedMonth: Int,
|
||||||
|
titlePaint: Paint,
|
||||||
|
subtitlePaint: Paint
|
||||||
|
) {
|
||||||
|
// Draw decorative header background
|
||||||
|
val headerPath = Path().apply {
|
||||||
|
moveTo(0f, 0f)
|
||||||
|
lineTo(pageWidth, 0f)
|
||||||
|
lineTo(pageWidth, 120f)
|
||||||
|
quadTo(pageWidth/2, 140f, 0f, 120f)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.drawPath(headerPath, Paint().apply {
|
||||||
|
color = primaryColor
|
||||||
|
alpha = 20
|
||||||
|
})
|
||||||
|
|
||||||
|
// Draw company name
|
||||||
|
canvas.drawText(companyName, pageWidth/2, 70f, titlePaint)
|
||||||
|
|
||||||
|
// Draw report title
|
||||||
|
val monthName = SimpleDateFormat("MMMM", Locale.getDefault())
|
||||||
|
.format(Calendar.getInstance().apply {
|
||||||
|
set(Calendar.MONTH, selectedMonth - 1)
|
||||||
|
}.time)
|
||||||
|
|
||||||
|
canvas.drawText(
|
||||||
|
"Monthly Attendance Summary",
|
||||||
|
pageWidth/2,
|
||||||
|
105f,
|
||||||
|
subtitlePaint
|
||||||
|
)
|
||||||
|
|
||||||
|
canvas.drawText(
|
||||||
|
"$monthName $selectedYear",
|
||||||
|
pageWidth/2,
|
||||||
|
135f,
|
||||||
|
subtitlePaint
|
||||||
|
)
|
||||||
|
|
||||||
|
currentY = 180f
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawTableHeaders(headerPaint: Paint) {
|
||||||
|
val headerHeight = 45f
|
||||||
|
|
||||||
|
// Create gradient for header background
|
||||||
|
val gradient = LinearGradient(
|
||||||
|
margin, currentY,
|
||||||
|
margin, currentY + headerHeight,
|
||||||
|
primaryColor,
|
||||||
|
secondaryColor,
|
||||||
|
Shader.TileMode.CLAMP
|
||||||
|
)
|
||||||
|
|
||||||
|
// Draw header background with gradient
|
||||||
|
canvas.drawRoundRect(
|
||||||
|
margin, currentY,
|
||||||
|
pageWidth - margin,
|
||||||
|
currentY + headerHeight,
|
||||||
|
8f, 8f,
|
||||||
|
Paint().apply { shader = gradient }
|
||||||
|
)
|
||||||
|
|
||||||
|
// Draw column headers
|
||||||
|
var xPos = margin + 15f
|
||||||
|
canvas.drawText("Employee Name", xPos, currentY + 30f, headerPaint)
|
||||||
|
|
||||||
|
xPos += 180f
|
||||||
|
canvas.drawText("Days Present", xPos, currentY + 30f, headerPaint)
|
||||||
|
|
||||||
|
xPos += 100f
|
||||||
|
canvas.drawText("Days Absent", xPos, currentY + 30f, headerPaint)
|
||||||
|
|
||||||
|
xPos += 100f
|
||||||
|
canvas.drawText("Attendance Dates", xPos, currentY + 30f, headerPaint)
|
||||||
|
|
||||||
|
currentY += headerHeight + 5f
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawEmployeeRow(
|
||||||
|
employee: Employee,
|
||||||
|
presentDates: List<Int>,
|
||||||
|
absentDays: Int,
|
||||||
|
workingDays: Int,
|
||||||
|
regularPaint: Paint,
|
||||||
|
highlightPaint: Paint
|
||||||
|
) {
|
||||||
|
val rowHeight = 60f
|
||||||
|
|
||||||
|
// Draw row background
|
||||||
|
canvas.drawRoundRect(
|
||||||
|
margin, currentY,
|
||||||
|
pageWidth - margin,
|
||||||
|
currentY + rowHeight,
|
||||||
|
4f, 4f,
|
||||||
|
Paint().apply {
|
||||||
|
color = backgroundColor
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Draw employee name
|
||||||
|
canvas.drawText(
|
||||||
|
employee.Name,
|
||||||
|
margin + 15f,
|
||||||
|
currentY + 10f,
|
||||||
|
regularPaint
|
||||||
|
)
|
||||||
|
|
||||||
|
// Draw days present
|
||||||
|
canvas.drawText(
|
||||||
|
"${presentDates.size} days",
|
||||||
|
margin + 195f,
|
||||||
|
currentY + 10f,
|
||||||
|
highlightPaint
|
||||||
|
)
|
||||||
|
|
||||||
|
// Draw days absent
|
||||||
|
canvas.drawText(
|
||||||
|
"$absentDays days",
|
||||||
|
margin + 295f,
|
||||||
|
currentY + 10f,
|
||||||
|
highlightPaint
|
||||||
|
)
|
||||||
|
|
||||||
|
// Draw attendance dates
|
||||||
|
val datesText = presentDates.chunked(5)
|
||||||
|
.joinToString("\n") { chunk ->
|
||||||
|
chunk.joinToString(", ") {
|
||||||
|
String.format("%02d", it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var dateY = currentY + 10f
|
||||||
|
datesText.split("\n").forEach { line ->
|
||||||
|
canvas.drawText(
|
||||||
|
line,
|
||||||
|
margin + 395f,
|
||||||
|
dateY,
|
||||||
|
regularPaint
|
||||||
|
)
|
||||||
|
dateY += 10f
|
||||||
|
}
|
||||||
|
|
||||||
|
currentY += rowHeight + 10f
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawFooter(paint: Paint) {
|
||||||
|
paint.textAlign = Paint.Align.CENTER
|
||||||
|
paint.textSize = 10f
|
||||||
|
canvas.drawText(
|
||||||
|
"Page $pageNumber",
|
||||||
|
pageWidth/2,
|
||||||
|
pageHeight - margin/2,
|
||||||
|
paint
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveDocument(selectedYear: Int, selectedMonth: Int): File {
|
||||||
val fileName = "Attendance_Summary_${selectedYear}_${selectedMonth}.pdf"
|
val fileName = "Attendance_Summary_${selectedYear}_${selectedMonth}.pdf"
|
||||||
val file = File(
|
val file = File(
|
||||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
|
||||||
@@ -165,58 +335,18 @@ class PDFGenerator(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun createNewPage() {
|
private fun createNewPage() {
|
||||||
val pageInfo = PdfDocument.PageInfo.Builder(pageWidth.toInt(), pageHeight.toInt(), pageNumber++).create()
|
val pageInfo = PdfDocument.PageInfo.Builder(
|
||||||
|
pageWidth.toInt(),
|
||||||
|
pageHeight.toInt(),
|
||||||
|
pageNumber++
|
||||||
|
).create()
|
||||||
|
|
||||||
currentPage = document.startPage(pageInfo)
|
currentPage = document.startPage(pageInfo)
|
||||||
canvas = currentPage.canvas
|
canvas = currentPage.canvas
|
||||||
currentY = 120f
|
currentY = margin
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun finishCurrentPage() {
|
private fun finishCurrentPage() {
|
||||||
document.finishPage(currentPage)
|
document.finishPage(currentPage)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun drawPageHeaders(
|
|
||||||
headerPaint: Paint,
|
|
||||||
subHeaderPaint: Paint,
|
|
||||||
selectedYear: Int,
|
|
||||||
selectedMonth: Int
|
|
||||||
) {
|
|
||||||
// Draw company logo background
|
|
||||||
val logoBgPaint = Paint().apply {
|
|
||||||
color = primaryColor
|
|
||||||
alpha = 30
|
|
||||||
}
|
|
||||||
canvas.drawCircle(pageWidth / 2f, 45f, 100f, logoBgPaint)
|
|
||||||
|
|
||||||
// Draw company name
|
|
||||||
canvas.drawText(companyName, pageWidth / 2f, 60f, headerPaint)
|
|
||||||
|
|
||||||
// Draw report title
|
|
||||||
val monthName = SimpleDateFormat("MMMM", Locale.getDefault())
|
|
||||||
.format(Calendar.getInstance().apply {
|
|
||||||
set(Calendar.MONTH, selectedMonth - 1)
|
|
||||||
}.time)
|
|
||||||
|
|
||||||
val reportTitle = "Monthly Attendance Summary - $monthName $selectedYear"
|
|
||||||
canvas.drawText(reportTitle, pageWidth / 2f, 95f, subHeaderPaint)
|
|
||||||
currentY = 140f
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun drawTableHeaders(headerPaint: Paint) {
|
|
||||||
// Draw header background
|
|
||||||
val headerBgPaint = Paint().apply {
|
|
||||||
color = primaryColor
|
|
||||||
}
|
|
||||||
|
|
||||||
val headerHeight = 40f
|
|
||||||
canvas.drawRect(startX, currentY, pageWidth - startX, currentY + headerHeight, headerBgPaint)
|
|
||||||
|
|
||||||
// Draw column headers
|
|
||||||
val textY = currentY + 25f
|
|
||||||
canvas.drawText("Employee Name", startX + 10f, textY, headerPaint)
|
|
||||||
canvas.drawText("Present Days", startX + 190f, textY, headerPaint)
|
|
||||||
canvas.drawText("Dates Present", startX + 290f, textY, headerPaint)
|
|
||||||
|
|
||||||
currentY += headerHeight
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
package `in`.sminnovations.smiadmin.ui.utils
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.app.ProgressDialog
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Environment
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.google.firebase.Firebase
|
||||||
|
import com.google.firebase.firestore.firestore
|
||||||
|
import `in`.sminnovations.smiadmin.data.Constant
|
||||||
|
import `in`.sminnovations.smiadmin.data.Employee
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.tasks.await
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.Calendar
|
||||||
|
|
||||||
|
class dailyReportExport(private val fragment: Fragment) {
|
||||||
|
private val db = Firebase.firestore
|
||||||
|
private var currentExcelTask: Job? = null
|
||||||
|
|
||||||
|
// Add permission check
|
||||||
|
private fun checkStoragePermission(): Boolean {
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
true // Android 10 and above don't need explicit storage permission for Downloads
|
||||||
|
} else {
|
||||||
|
ContextCompat.checkSelfPermission(
|
||||||
|
fragment.requireContext(),
|
||||||
|
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||||
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun exportAttendance(date : Calendar, source: String) {
|
||||||
|
if (!checkStoragePermission()) {
|
||||||
|
ActivityCompat.requestPermissions(
|
||||||
|
fragment.requireActivity(),
|
||||||
|
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
|
||||||
|
STORAGE_PERMISSION_CODE
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel any existing task
|
||||||
|
currentExcelTask?.cancel()
|
||||||
|
|
||||||
|
val progressDialog = ProgressDialog(fragment.requireContext()).apply {
|
||||||
|
setMessage("Generating attendance report...")
|
||||||
|
setCancelable(false)
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
|
||||||
|
currentExcelTask = fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
try {
|
||||||
|
val company = if (source == "SMI"){
|
||||||
|
Constant.COMPANY_NAME
|
||||||
|
}else{
|
||||||
|
"IISc-OMI_LAB"
|
||||||
|
}
|
||||||
|
// First get all employees
|
||||||
|
val employeesSnapshot = withContext(Dispatchers.IO) {
|
||||||
|
db.collection("employees")
|
||||||
|
.whereEqualTo("EmployeeStatus", true)
|
||||||
|
.whereEqualTo("from",source)
|
||||||
|
.get()
|
||||||
|
.await()
|
||||||
|
}
|
||||||
|
|
||||||
|
val employees = employeesSnapshot.documents.map { doc ->
|
||||||
|
Employee(
|
||||||
|
Id = doc.getString("Id") ?: doc.getString("id") ?: "",
|
||||||
|
Name = doc.getString("Name") ?: doc.getString("name") ?: "",
|
||||||
|
)
|
||||||
|
}.sortedBy { it.Name }
|
||||||
|
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val pdfGenerator = PDFGenerator_daily(fragment.requireContext(), company)
|
||||||
|
pdfGenerator.generateDailyReport(employees, selectedDate = date,source = source)
|
||||||
|
}.also { pdfFile ->
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
// Verify file exists and is readable
|
||||||
|
if (!pdfFile.exists() || !pdfFile.canRead()) {
|
||||||
|
throw IOException("Generated PDF file is not accessible")
|
||||||
|
}
|
||||||
|
|
||||||
|
val downloadsDir = Environment.getExternalStoragePublicDirectory(
|
||||||
|
Environment.DIRECTORY_DOWNLOADS)
|
||||||
|
val pdfFileInDownloads = File(downloadsDir, pdfFile.name)
|
||||||
|
|
||||||
|
// Copy file to Downloads if needed
|
||||||
|
if (!pdfFileInDownloads.exists()) {
|
||||||
|
pdfFile.copyTo(pdfFileInDownloads, overwrite = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the file was copied successfully
|
||||||
|
if (!pdfFileInDownloads.exists() || !pdfFileInDownloads.canRead()) {
|
||||||
|
throw IOException("Failed to copy PDF to Downloads folder")
|
||||||
|
}
|
||||||
|
|
||||||
|
val emailIntent = Intent(Intent.ACTION_SEND).apply {
|
||||||
|
type = "application/pdf"
|
||||||
|
putExtra(Intent.EXTRA_EMAIL, arrayOf("admin@sminnovations.in"))
|
||||||
|
putExtra(Intent.EXTRA_SUBJECT, "Attendance Report - ")
|
||||||
|
putExtra(Intent.EXTRA_TEXT, "Please find attached the attendance report.")
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Convert file URI to content URI for sharing
|
||||||
|
val contentUri = FileProvider.getUriForFile(
|
||||||
|
fragment.requireContext(),
|
||||||
|
"${fragment.requireContext().packageName}.provider",
|
||||||
|
pdfFileInDownloads
|
||||||
|
)
|
||||||
|
putExtra(Intent.EXTRA_STREAM, contentUri)
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
throw IOException("FileProvider not properly configured: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch to Main dispatcher for starting the email activity
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
progressDialog.dismiss()
|
||||||
|
try {
|
||||||
|
fragment.startActivity(Intent.createChooser(emailIntent, "Send Email"))
|
||||||
|
Toast.makeText(
|
||||||
|
fragment.context,
|
||||||
|
"Report saved in Downloads folder: ${pdfFile.name}",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
} catch (e: ActivityNotFoundException) {
|
||||||
|
Toast.makeText(
|
||||||
|
fragment.context,
|
||||||
|
"No email app found",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
progressDialog.dismiss()
|
||||||
|
Toast.makeText(
|
||||||
|
fragment.context,
|
||||||
|
"Error sending email: ${e.message}",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
progressDialog.dismiss()
|
||||||
|
Toast.makeText(
|
||||||
|
fragment.context,
|
||||||
|
"Error: ${e.localizedMessage ?: "Unknown error occurred"}",
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cleanup() {
|
||||||
|
currentExcelTask?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val STORAGE_PERMISSION_CODE = 101
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||||
|
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M7,10l5,5 5,-5z"/>
|
||||||
|
|
||||||
|
</vector>
|
||||||
5
app/src/main/res/drawable/baseline_share_24.xml
Normal file
5
app/src/main/res/drawable/baseline_share_24.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||||
|
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/>
|
||||||
|
|
||||||
|
</vector>
|
||||||
BIN
app/src/main/res/drawable/smi_logo.png
Normal file
BIN
app/src/main/res/drawable/smi_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
23
app/src/main/res/layout/dialog_date_month_year_picker.xml
Normal file
23
app/src/main/res/layout/dialog_date_month_year_picker.xml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/dailyExport"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:autoSizeTextType="uniform"
|
||||||
|
android:background="#E49D9D"
|
||||||
|
android:text="Make sure you are Exporting Report at the end of the day"
|
||||||
|
android:textSize="20sp" />
|
||||||
|
|
||||||
|
<CalendarView
|
||||||
|
android:id="@+id/calendarViewPicker"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
41
app/src/main/res/layout/dialog_report_source.xml
Normal file
41
app/src/main/res/layout/dialog_report_source.xml
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvTitle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Select Report Source"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:paddingBottom="8dp" />
|
||||||
|
|
||||||
|
<RadioGroup
|
||||||
|
android:id="@+id/rgSource"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/rbSMI"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="SMI" />
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/rbIISC"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="IISC" />
|
||||||
|
</RadioGroup>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnDownload"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Download" />
|
||||||
|
</LinearLayout>
|
||||||
122
app/src/main/res/layout/fragment_add_employee.xml
Normal file
122
app/src/main/res/layout/fragment_add_employee.xml
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="15dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/etEmployeeName"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="Employee Name" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/etEmployeeEmail"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="Email"
|
||||||
|
android:inputType="textEmailAddress" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/etEmployeePhone"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="Phone Number"
|
||||||
|
android:maxLength="10"
|
||||||
|
android:inputType="phone" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/etEmployeePosition"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="Designation" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu">
|
||||||
|
|
||||||
|
<AutoCompleteTextView
|
||||||
|
android:id="@+id/etEmployeeFrom"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="none"
|
||||||
|
android:hint="Working From" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/etDateOfBirth"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="Date of Birth"
|
||||||
|
android:focusable="false"
|
||||||
|
android:clickable="true" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/etDateOfJoining"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="Date of Joining"
|
||||||
|
android:focusable="false"
|
||||||
|
android:clickable="true" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnSubmit"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
android:text="Submit" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</ScrollView>
|
||||||
@@ -18,11 +18,18 @@
|
|||||||
android:id="@+id/spinnerMonth"
|
android:id="@+id/spinnerMonth"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="16dp"
|
android:layout_marginStart="5dp"
|
||||||
android:layout_marginEnd="16dp"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/textViewEmployeeName" />
|
app:layout_constraintTop_toBottomOf="@id/textViewEmployeeName" />
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/spinnerYear"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="5dp"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/spinnerMonth"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/spinnerMonth" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/textViewCount"
|
android:id="@+id/textViewCount"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
|||||||
@@ -4,10 +4,22 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.SearchView
|
||||||
|
android:id="@+id/searchView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:iconifiedByDefault="false"
|
||||||
|
app:queryHint="Search by name"/>
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/recyclerViewEmployees"
|
android:id="@+id/recyclerViewEmployees"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="0dp"
|
||||||
|
android:layout_marginTop="15dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/searchView"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
|
||||||
|
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
|
|||||||
@@ -3,11 +3,28 @@
|
|||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
<androidx.appcompat.widget.SearchView
|
||||||
|
android:id="@+id/searchView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:iconifiedByDefault="false"
|
||||||
|
app:queryHint="Search by name"/>
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/spinnerSort"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/searchView" />
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:layout_marginTop="15dp"
|
||||||
android:id="@+id/recyclerViewEmployees"
|
android:id="@+id/recyclerViewEmployees"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/spinnerSort"
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
|
||||||
|
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
|
|||||||
@@ -76,6 +76,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:cardCornerRadius="8dp"
|
app:cardCornerRadius="8dp"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
app:cardElevation="4dp">
|
app:cardElevation="4dp">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
@@ -98,6 +99,88 @@
|
|||||||
android:text="Download attendance reports" />
|
android:text="Download attendance reports" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</androidx.cardview.widget.CardView>
|
</androidx.cardview.widget.CardView>
|
||||||
|
<androidx.cardview.widget.CardView
|
||||||
|
android:id="@+id/cardDownloadDailyReport"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
app:cardElevation="4dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Download Daily Report"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="Download daily attendance reports" />
|
||||||
|
</LinearLayout>
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
|
<androidx.cardview.widget.CardView
|
||||||
|
android:id="@+id/cardAddEmployee"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
app:cardElevation="4dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Add Employee"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="Add employee to the system" />
|
||||||
|
</LinearLayout>
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
|
<androidx.cardview.widget.CardView
|
||||||
|
android:id="@+id/cardEmployeeQr"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:cardElevation="4dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Share QR Code"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="Share unique Qr code with Employees" />
|
||||||
|
</LinearLayout>
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|||||||
81
app/src/main/res/layout/fragment_login.xml
Normal file
81
app/src/main/res/layout/fragment_login.xml
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="fill_parent"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/parentLayout"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_horizontal|center_vertical"
|
||||||
|
android:layout_marginTop="100.dp"
|
||||||
|
android:background="@drawable/smi_logo"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:visibility="visible" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/childLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="32dp"
|
||||||
|
android:text="Admin Login"
|
||||||
|
android:textSize="24sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="16dp">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/etMobile"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="Mobile Number"
|
||||||
|
android:inputType="phone"
|
||||||
|
android:maxLength="10" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="24dp"
|
||||||
|
app:passwordToggleEnabled="true">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/etPassword"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="Password"
|
||||||
|
android:inputType="textPassword" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btnLogin"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="12dp"
|
||||||
|
android:text="Login" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progressBar"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
@@ -47,4 +47,11 @@
|
|||||||
android:text="SMI Admin App(Employee Attendance)"
|
android:text="SMI Admin App(Employee Attendance)"
|
||||||
android:layout_marginTop="4dp" />
|
android:layout_marginTop="4dp" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Logout"
|
||||||
|
android:layout_marginTop="30dp"
|
||||||
|
android:id="@+id/logOutBtn"/>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
57
app/src/main/res/layout/fragment_share_qr.xml
Normal file
57
app/src/main/res/layout/fragment_share_qr.xml
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
<androidx.appcompat.widget.SearchView
|
||||||
|
android:id="@+id/searchView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:iconifiedByDefault="false"
|
||||||
|
app:queryHint="Search by name"/>
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/spinnerSort"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/searchView" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recyclerViewEmployees"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:layout_marginTop="15dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/spinnerSort"
|
||||||
|
tools:listitem="@layout/item_employee_qr" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progressBar"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textEmpty"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="No employees found"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -1,42 +1,71 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.cardview.widget.CardView
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
android:id="@+id/cardView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="8dp"
|
android:layout_margin="8dp"
|
||||||
app:cardCornerRadius="8dp"
|
app:cardCornerRadius="8dp"
|
||||||
app:cardElevation="4dp">
|
app:cardElevation="4dp"
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical">
|
||||||
android:padding="16dp">
|
|
||||||
|
|
||||||
<TextView
|
<!-- Header Layout -->
|
||||||
android:id="@+id/textViewDate"
|
<RelativeLayout
|
||||||
android:layout_width="wrap_content"
|
android:id="@+id/headerLayout"
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:textSize="16sp"
|
|
||||||
android:textStyle="bold" />
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="8dp"
|
android:padding="16dp"
|
||||||
android:orientation="horizontal">
|
android:background="?android:attr/selectableItemBackground">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/textViewCheckIn"
|
android:id="@+id/textViewDate"
|
||||||
android:layout_width="0dp"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1" />
|
android:layout_alignParentStart="true"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
/>
|
||||||
|
|
||||||
<TextView
|
<ImageView
|
||||||
android:id="@+id/textViewCheckOut"
|
android:id="@+id/imageViewArrow"
|
||||||
android:layout_width="0dp"
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:src="@drawable/baseline_arrow_drop_down_24"
|
||||||
|
android:rotation="0" />
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
<!-- Divider -->
|
||||||
|
<View
|
||||||
|
android:id="@+id/divider"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:background="?android:attr/listDivider"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<!-- Expandable Content -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/expandableLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/layoutCheckIns"
|
||||||
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1" />
|
android:orientation="vertical" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</androidx.cardview.widget.CardView>
|
</androidx.cardview.widget.CardView>
|
||||||
21
app/src/main/res/layout/item_checkin_checkout_pair.xml
Normal file
21
app/src/main/res/layout/item_checkin_checkout_pair.xml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="8dp"
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textViewCheckIn"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="@android:color/black" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textViewCheckOut"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:textColor="@android:color/black" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
46
app/src/main/res/layout/item_employee_qr.xml
Normal file
46
app/src/main/res/layout/item_employee_qr.xml
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
app:cardElevation="4dp">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textEmployeeName"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/buttonShare"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textEmployeeId"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/buttonShare"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/textEmployeeName" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/buttonShare"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:src="@drawable/baseline_share_24"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
9
app/src/main/res/layout/list_item.xml
Normal file
9
app/src/main/res/layout/list_item.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<TextView
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textAppearance="?attr/textAppearanceSubtitle1" />
|
||||||
@@ -1,8 +1,20 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- mobile_navigation.xml -->
|
||||||
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:id="@+id/mobile_navigation"
|
android:id="@+id/mobile_navigation"
|
||||||
app:startDestination="@+id/navigation_home">
|
app:startDestination="@+id/navigation_login">
|
||||||
|
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/navigation_login"
|
||||||
|
android:name="in.sminnovations.smiadmin.ui.fragments.LoginFragment"
|
||||||
|
android:label="Login">
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_login_to_home"
|
||||||
|
app:destination="@id/navigation_home"
|
||||||
|
app:popUpTo="@id/navigation_login"
|
||||||
|
app:popUpToInclusive="true" />
|
||||||
|
</fragment>
|
||||||
|
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/navigation_home"
|
android:id="@+id/navigation_home"
|
||||||
@@ -14,12 +26,24 @@
|
|||||||
<action
|
<action
|
||||||
android:id="@+id/action_home_to_employee_attendance_list"
|
android:id="@+id/action_home_to_employee_attendance_list"
|
||||||
app:destination="@id/navigation_employee_attendance_list" />
|
app:destination="@id/navigation_employee_attendance_list" />
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_home_to_add_employee"
|
||||||
|
app:destination="@id/navigation_add_employee" />
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_to_employeeQrFragment"
|
||||||
|
app:destination="@id/navigation_employee_qr" />
|
||||||
</fragment>
|
</fragment>
|
||||||
|
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/navigation_settings"
|
android:id="@+id/navigation_settings"
|
||||||
android:name="in.sminnovations.smiadmin.ui.fragments.SettingsFragment"
|
android:name="in.sminnovations.smiadmin.ui.fragments.SettingsFragment"
|
||||||
android:label="Settings" />
|
android:label="Settings">
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_settings_to_login"
|
||||||
|
app:destination="@id/navigation_login"
|
||||||
|
app:popUpTo="@id/mobile_navigation"
|
||||||
|
app:popUpToInclusive="true" />
|
||||||
|
</fragment>
|
||||||
|
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/navigation_employee_list"
|
android:id="@+id/navigation_employee_list"
|
||||||
@@ -35,6 +59,28 @@
|
|||||||
app:destination="@id/navigation_attendance_detail" />
|
app:destination="@id/navigation_attendance_detail" />
|
||||||
</fragment>
|
</fragment>
|
||||||
|
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/navigation_add_employee"
|
||||||
|
android:name="in.sminnovations.smiadmin.ui.fragments.AddEmployeeFragment"
|
||||||
|
android:label="Add Employee">
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_add_employee_to_home"
|
||||||
|
app:destination="@id/navigation_home"
|
||||||
|
app:popUpTo="@id/navigation_home"
|
||||||
|
app:popUpToInclusive="false" />
|
||||||
|
</fragment>
|
||||||
|
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/navigation_employee_qr"
|
||||||
|
android:name="in.sminnovations.smiadmin.ui.fragments.ShareQrFragment"
|
||||||
|
android:label="Employee Qr">
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_employee_qr_to_home"
|
||||||
|
app:destination="@id/navigation_home"
|
||||||
|
app:popUpTo="@id/navigation_home"
|
||||||
|
app:popUpToInclusive="false" />
|
||||||
|
</fragment>
|
||||||
|
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/navigation_attendance_detail"
|
android:id="@+id/navigation_attendance_detail"
|
||||||
android:name="in.sminnovations.smiadmin.ui.fragments.attendance.AttendanceDetailFragment"
|
android:name="in.sminnovations.smiadmin.ui.fragments.attendance.AttendanceDetailFragment"
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<paths>
|
<paths>
|
||||||
|
<external-path
|
||||||
|
name="external_downloads"
|
||||||
|
path="Download" />
|
||||||
<external-files-path
|
<external-files-path
|
||||||
name="external_files"
|
name="external_files"
|
||||||
path="." />
|
path="." />
|
||||||
|
<cache-path
|
||||||
|
name="shared_qr"
|
||||||
|
path="." />
|
||||||
</paths>
|
</paths>
|
||||||
Reference in New Issue
Block a user