24 Commits
dev ... main

Author SHA1 Message Date
sathwikcs
6ed17b3e07 updated the PDFGenerator daily.kt 2025-04-08 12:28:38 +05:30
sathwikcs
c9db1cb727 updated the PDFGenerator daily.kt 2025-03-24 15:08:37 +05:30
sathwikcs
5489aeb588 updated the dialog_date_month_year_picker.xml 2025-03-21 18:03:34 +05:30
sathwikcs
1cebe79071 Fixed the logo in splash screen 2025-03-21 18:03:06 +05:30
sathwikcs
aed649bf7d updated DailyMonthYearPickerDialog 2025-03-21 18:02:34 +05:30
sathwikcs
e2e5733d4b updated build.gradle 2025-03-21 18:01:36 +05:30
sathwikcs
419e628d06 updated other.xml 2025-03-18 17:45:29 +05:30
sathwikcs
b269993e13 updated PDFGenerator daily.kt 2025-03-18 17:44:55 +05:30
sathwikcs
53149d0361 created PDFGenerator daily.kt 2025-03-18 17:44:14 +05:30
sathwikcs
811f71049d created dialog_date_month_year_picker.xml 2025-03-18 17:43:28 +05:30
sathwikcs
9c3156a85a created DailyMonthYearPickerDialog 2025-03-18 17:42:48 +05:30
sathwikcs
2867ecce4f created dailyReportExport.kt 2025-03-18 17:42:13 +05:30
sathwikcs
6c90a1a183 added the home fragment_home.xml 2025-03-18 17:41:46 +05:30
sathwikcs
43d6979fac Fixed the issue when adding employee 2025-03-03 13:09:46 +05:30
sathwikcs
43317574e9 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	.idea/other.xml
#	app/src/main/java/in/sminnovations/smiadmin/data/Employee.kt
2025-03-03 13:09:08 +05:30
sathwikcs
b7d0fd32a9 Fixed the issue when adding employee 2025-03-03 13:05:08 +05:30
chandrashekhar reddy
a72c062b96 expandable attendance list bug fix 2025-02-21 13:14:56 +05:30
chandrashekhar reddy
304f2cbe60 expandable attendance list and network check 2025-02-12 11:14:57 +05:30
chandrashekhar reddy
5d691e9b1b PDFGenerator updated the format 2025-02-05 14:32:03 +05:30
chandrashekhar reddy
43c3137041 changes according to smi + iisc, added login screen, added filer, improved the UI 2025-02-04 17:27:33 +05:30
chandrashekhar reddy
830d347606 added fragments to add employee, download report in pdf format and send over mail, sharable qr codes 2025-01-31 13:59:02 +05:30
chandrashekhar reddy
27dd707e21 Merge remote-tracking branch 'refs/remotes/origin/dev' 2025-01-20 12:38:22 +05:30
sathwikcs
3e15b0df6b Updated the pdf generator, and improved pdf design 2025-01-17 16:41:23 +05:30
sathwikcs
3bb2b58ce1 Updated the pdf generator, and improved pdf design 2025-01-17 16:35:52 +05:30
45 changed files with 3194 additions and 224 deletions

1
.idea/misc.xml generated
View File

@@ -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
View File

@@ -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>

View File

@@ -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'
} }

View File

@@ -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">

View 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()
}
}

View File

@@ -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 ->

View File

@@ -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
) )

View File

@@ -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"
) )

View File

@@ -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("-") }
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 day = parts[0].toInt()
val month = parts[1].toInt() val month = parts[1].toInt()
val year = parts[2].toInt() val year = parts[2].toInt()
String.format("%04d%02d%02d", year, month, day) return String.format("%04d%02d%02d", year, month, day)
})
notifyDataSetChanged()
}
} }

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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)
} }

View File

@@ -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
}
}

View File

@@ -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()
} }
} }

View File

@@ -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
}
}

View File

@@ -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)
}
} }

View File

@@ -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
)
}
}

View 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) {
// Get all fields from the document
val data = doc.data ?: return@mapNotNull null
// 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( AttendanceData(
date = doc.id, date = doc.id,
checkIn = doc.getString("checkIn") ?: "N/A", checkIns = checkIns,
checkOut = doc.getString("checkOut") ?: "N/A" 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
} }
} }

View File

@@ -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 ->
val status = doc.getBoolean("EmployeeStatus") ?: true // Default to true if field is missing
if (status) {
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") ?: "",
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

View File

@@ -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)
} }
} }

View File

@@ -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
}
}

View File

@@ -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) {
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 - $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) { withContext(Dispatchers.Main) {
progressDialog.dismiss() progressDialog.dismiss()
try {
fragment.startActivity(Intent.createChooser(emailIntent, "Send Email"))
Toast.makeText( Toast.makeText(
fragment.context, fragment.context,
"Report saved in Downloads folder: ${pdfFile.name}", "Report saved in Downloads folder: ${pdfFile.name}",
Toast.LENGTH_LONG Toast.LENGTH_LONG
).show() ).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
}
} }

View File

@@ -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
}
}
}

View File

@@ -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)
}
}

View File

@@ -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 // Add footer with page numbers
val borderPaint = Paint().apply { drawFooter(regularPaint)
style = Paint.Style.STROKE
strokeWidth = 0.5f
color = borderColor
}
canvas.drawLine(
startX, currentY + rowHeight,
pageWidth - startX, currentY + rowHeight,
borderPaint
)
currentY += rowHeight
}
// 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
}
} }

View File

@@ -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
}
}

View 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="M7,10l5,5 5,-5z"/>
</vector>

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View 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>

View 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>

View 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>

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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>

View 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>

View File

@@ -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>

View 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>

View File

@@ -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">
<!-- Header Layout -->
<RelativeLayout
android:id="@+id/headerLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:background="?android:attr/selectableItemBackground">
<TextView <TextView
android:id="@+id/textViewDate" android:id="@+id/textViewDate"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textSize="16sp" android:layout_alignParentStart="true"
android:textStyle="bold" /> android:layout_centerVertical="true"
android:textSize="18sp"
android:textStyle="bold"
/>
<ImageView
android:id="@+id/imageViewArrow"
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 <LinearLayout
android:id="@+id/expandableLayout"
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:orientation="vertical"
android:orientation="horizontal"> android:visibility="gone"
android:padding="16dp">
<TextView <LinearLayout
android:id="@+id/textViewCheckIn" android:id="@+id/layoutCheckIns"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" /> android:orientation="vertical" />
<TextView
android:id="@+id/textViewCheckOut"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
</androidx.cardview.widget.CardView> </androidx.cardview.widget.CardView>

View 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>

View 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>

View 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" />

View File

@@ -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"

View File

@@ -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>