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">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<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="screenY" value="1280" />
</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>
<option name="api" value="34" />
<option name="brand" value="OPPO" />
@@ -47,6 +58,28 @@
<option name="screenX" value="1840" />
<option name="screenY" value="2944" />
</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>
<option name="api" value="31" />
<option name="brand" value="samsung" />
@@ -69,6 +102,28 @@
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</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>
<option name="api" value="33" />
<option name="brand" value="samsung" />
@@ -124,6 +179,17 @@
<option name="screenX" value="2220" />
<option name="screenY" value="1080" />
</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>
<option name="api" value="34" />
<option name="brand" value="samsung" />
@@ -146,6 +212,28 @@
<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="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>
<option name="api" value="33" />
<option name="brand" value="google" />
@@ -179,6 +267,50 @@
<option name="screenX" value="2208" />
<option name="screenY" value="1840" />
</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>
<option name="api" value="33" />
<option name="brand" value="samsung" />
@@ -190,6 +322,28 @@
<option name="screenX" value="1848" />
<option name="screenY" value="2960" />
</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>
<option name="api" value="34" />
<option name="brand" value="google" />
@@ -234,6 +388,28 @@
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</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>
<option name="api" value="31" />
<option name="brand" value="google" />
@@ -290,6 +466,17 @@
<option name="screenY" value="384" />
<option name="type" value="WEAR_OS" />
</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>
<option name="api" value="30" />
<option name="brand" value="google" />
@@ -312,6 +499,17 @@
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</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>
<option name="api" value="33" />
<option name="brand" value="google" />
@@ -334,6 +532,17 @@
<option name="screenX" value="1080" />
<option name="screenY" value="2424" />
</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>
</option>
</component>

View File

@@ -13,8 +13,8 @@ android {
applicationId "in.sminnovations.smiadmin"
minSdk 26
targetSdk 34
versionCode 2
versionName "1.1"
versionCode 7
versionName "1.8-beta"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@@ -55,5 +55,6 @@ dependencies {
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'com.google.android.material:material:1.11.0'
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:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:icon="@drawable/smi_logo"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:roundIcon="@drawable/smi_logo"
android:supportsRtl="true"
android:theme="@style/Theme.SMIAdmin"
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
import android.os.Bundle
import android.view.View
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
@@ -12,7 +13,7 @@ import androidx.navigation.ui.setupWithNavController
import com.google.android.material.bottomnavigation.BottomNavigationView
import `in`.sminnovations.smiadmin.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
class MainActivity : BaseActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var navController: NavController
@@ -29,7 +30,21 @@ class MainActivity : AppCompatActivity() {
navController = navHostFragment.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
binding.navView.setOnItemSelectedListener { menuItem ->

View File

@@ -2,6 +2,7 @@ package `in`.sminnovations.smiadmin.data
data class AttendanceData(
val date: String = "",
val checkIn: String = "",
val checkOut: String = ""
val checkIns: Map<String, String>,
val checkOuts: Map<String, String>,
var isExpanded: Boolean = false
)

View File

@@ -1,7 +1,41 @@
package `in`.sminnovations.smiadmin.data
import com.google.firebase.firestore.PropertyName
data class Employee(
val Id: String = "",
val Name: String = "",
var EmployeeStatus: Boolean = false
@PropertyName("Id")
var Id: String = "",
@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
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import androidx.cardview.widget.CardView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import `in`.sminnovations.smiadmin.R
import `in`.sminnovations.smiadmin.data.AttendanceData
@@ -13,8 +17,9 @@ class AttendanceAdapter : RecyclerView.Adapter<AttendanceAdapter.ViewHolder>() {
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val dateTextView: TextView = itemView.findViewById(R.id.textViewDate)
val checkInTextView: TextView = itemView.findViewById(R.id.textViewCheckIn)
val checkOutTextView: TextView = itemView.findViewById(R.id.textViewCheckOut)
val checkInsLayout: LinearLayout = itemView.findViewById(R.id.layoutCheckIns)
val expandableLayout: LinearLayout = itemView.findViewById(R.id.expandableLayout)
val cardView: CardView = itemView.findViewById(R.id.cardView)
}
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) {
val attendance = attendanceList[position]
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
// In AttendanceAdapter.kt
fun updateAttendance(newAttendance: List<AttendanceData>) {
val diffCallback = AttendanceDiffCallback(attendanceList, newAttendance)
val diffResult = DiffUtil.calculateDiff(diffCallback)
attendanceList.clear()
// Convert date string to comparable format and sort
attendanceList.addAll(newAttendance.sortedByDescending {
// Convert dd-MM-yyyy to comparable format
val parts = it.date.split("-")
val day = parts[0].toInt()
val month = parts[1].toInt()
val year = parts[2].toInt()
String.format("%04d%02d%02d", year, month, day)
})
notifyDataSetChanged()
attendanceList.addAll(newAttendance.sortedByDescending { it.getComparableDate() })
diffResult.dispatchUpdatesTo(this)
}
private fun displayAttendanceRecords(
context: Context,
holder: ViewHolder,
attendance: AttendanceData
) {
// Create a set of all unique check-in keys
val allCheckInKeys = attendance.checkIns.keys
// Handle numbered check-ins
for (i in 1..attendance.checkIns.size) {
val checkInKey = "checkIn$i"
val checkOutKey = "checkOut$i"
if (attendance.checkIns.containsKey(checkInKey)) {
addCheckInPair(
context,
holder,
checkInKey,
checkOutKey,
attendance.checkIns[checkInKey]!!,
attendance.checkOuts[checkOutKey]
)
}
}
// Handle non-numbered check-ins
allCheckInKeys
.filter { !it.matches(Regex("checkIn\\d+")) }
.forEach { checkInKey ->
val baseKey = checkInKey.replace("checkIn", "")
val checkOutKey = "checkOut$baseKey"
addCheckInPair(
context,
holder,
checkInKey,
checkOutKey,
attendance.checkIns[checkInKey]!!,
attendance.checkOuts[checkOutKey]
)
}
}
private fun addCheckInPair(
context: Context,
holder: ViewHolder,
checkInKey: String,
checkOutKey: String,
checkInValue: String,
checkOutValue: String?
) {
val pairLayout = LayoutInflater.from(context)
.inflate(R.layout.item_checkin_checkout_pair, holder.checkInsLayout, false)
val checkInView = pairLayout.findViewById<TextView>(R.id.textViewCheckIn)
val checkOutView = pairLayout.findViewById<TextView>(R.id.textViewCheckOut)
// Display the original key name for non-standard keys
val checkInLabel = if (checkInKey.matches(Regex("checkIn\\d+"))) {
"Check In"
} else {
"Check In (${checkInKey.replace("checkIn", "")})"
}
val checkOutLabel = if (checkOutKey.matches(Regex("checkOut\\d+"))) {
"Check Out"
} else {
"Check Out (${checkOutKey.replace("checkOut", "")})"
}
checkInView.text = "$checkInLabel: $checkInValue"
checkOutView.text = "$checkOutLabel: ${checkOutValue ?: "Pending"}"
holder.checkInsLayout.addView(pairLayout)
}
}
// DiffUtil callback for efficient updates
private class AttendanceDiffCallback(
private val oldList: List<AttendanceData>,
private val newList: List<AttendanceData>
) : DiffUtil.Callback() {
override fun getOldListSize() = oldList.size
override fun getNewListSize() = newList.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldList[oldItemPosition].date == newList[newItemPosition].date
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldList[oldItemPosition] == newList[newItemPosition]
}
}
// Extension function for AttendanceData to get comparable date
private fun AttendanceData.getComparableDate(): String {
val parts = date.split("-")
val day = parts[0].toInt()
val month = parts[1].toInt()
val year = parts[2].toInt()
return String.format("%04d%02d%02d", year, month, day)
}

View File

@@ -10,7 +10,7 @@ import `in`.sminnovations.smiadmin.R
import `in`.sminnovations.smiadmin.data.Employee
class EmployeeListAdapter(
private val employees: MutableList<Employee> = mutableListOf(),
val employees: MutableList<Employee> = mutableListOf(),
private val onStatusChanged: (Employee, Boolean) -> Unit
) : RecyclerView.Adapter<EmployeeListAdapter.EmployeeViewHolder>() {
@@ -32,7 +32,7 @@ class EmployeeListAdapter(
holder.statusSwitch.setOnCheckedChangeListener(null)
// Set the text and switch state
holder.nameTextView.text = employee.Name
holder.nameTextView.text = employee.Name + " (" + employee.from + ")"
holder.statusSwitch.isChecked = employee.EmployeeStatus
// 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) {
val employee = employees[position]
holder.nameTextView.text = employee.Name
holder.nameTextView.text = employee.Name + " (" + employee.from + ")"
holder.itemView.setOnClickListener {
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
import android.Manifest
import android.app.AlertDialog
import android.app.ProgressDialog
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
@@ -12,6 +14,8 @@ import android.provider.Settings
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.RadioGroup
import android.widget.Toast
import androidx.cardview.widget.CardView
import androidx.core.content.ContextCompat
@@ -19,12 +23,16 @@ import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import `in`.sminnovations.smiadmin.data.Constant.STORAGE_PERMISSION_CODE
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.MonthYearPickerDialog
import `in`.sminnovations.smiadmin.ui.utils.dailyReportExport
import kotlinx.coroutines.Job
import java.util.Calendar
class HomeFragment : Fragment() {
private lateinit var exportTask: ExportTask
private lateinit var dailyReportExport: dailyReportExport
private var currentExcelTask: Job? = null
override fun onCreateView(
@@ -33,9 +41,10 @@ class HomeFragment : Fragment() {
savedInstanceState: Bundle?
): View? {
exportTask = ExportTask(this)
dailyReportExport = dailyReportExport(this)
return inflater.inflate(R.layout.fragment_home, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@@ -46,7 +55,12 @@ class HomeFragment : Fragment() {
view.findViewById<CardView>(R.id.cardAttendanceReport).setOnClickListener {
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 {
if (checkStoragePermission()) {
showMonthYearPicker()
@@ -54,17 +68,60 @@ class HomeFragment : Fragment() {
requestStoragePermission()
}
}
view.findViewById<CardView>(R.id.cardDownloadDailyReport).setOnClickListener{
if(checkStoragePermission()){
showDailyMonthYearPicker()
}else{
requestStoragePermission()
}
}
}
private fun showMonthYearPicker() {
MonthYearPickerDialog().apply {
setOnDateSelectedListener { year, month ->
downloadAttendanceReport(year, month)
showReportDialog(year, month, requireActivity())
//downloadAttendanceReport(year, month)
}
}.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()
val progressDialog = ProgressDialog(requireContext()).apply {
@@ -72,10 +129,23 @@ class HomeFragment : Fragment() {
setCancelable(false)
show()
}
exportTask.exportAttendance(year, month)
exportTask.exportAttendance(year, month,source)
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 {
return if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
ContextCompat.checkSelfPermission(
@@ -118,6 +188,7 @@ class HomeFragment : Fragment() {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// Permission granted, now show the picker
showMonthYearPicker()
} else {
Toast.makeText(
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() {
super.onDestroy()
dailyReportExport.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
import android.content.Context
import android.content.pm.PackageManager
import android.os.Bundle
import android.view.LayoutInflater
@@ -7,6 +8,8 @@ import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import com.google.android.material.button.MaterialButton
import `in`.sminnovations.smiadmin.R
@@ -20,6 +23,10 @@ class SettingsFragment : Fragment() {
// Set app version
val versionTextView: TextView = root.findViewById(R.id.textViewVersion)
val button: MaterialButton = root.findViewById(R.id.logOutBtn)
button.setOnClickListener {
logout()
}
try {
val pInfo = requireContext().packageManager.getPackageInfo(
requireContext().packageName, 0
@@ -34,4 +41,12 @@ class SettingsFragment : Fragment() {
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.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -24,6 +25,9 @@ class AttendanceDetailFragment : Fragment() {
private val months = arrayOf("January", "February", "March", "April", "May", "June",
"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(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -41,11 +45,13 @@ class AttendanceDetailFragment : Fragment() {
binding.textViewEmployeeName.text = employeeName
setupMonthSpinner()
setupYearSpinner()
setupRecyclerView()
// Get current month (1-12)
// Get current month (1-12) and year
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() {
@@ -63,8 +69,35 @@ class AttendanceDetailFragment : Fragment() {
binding.spinnerMonth.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
val employeeId = arguments?.getString("employeeId") ?: return
// position + 1 because months in Calendar are 0-based
fetchAttendanceData(employeeId, position + 1)
val selectedYear = years[binding.spinnerYear.selectedItemPosition]
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<*>?) {}
@@ -76,7 +109,7 @@ class AttendanceDetailFragment : Fragment() {
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
db.collection("attendance_$employeeId")
@@ -87,16 +120,42 @@ class AttendanceDetailFragment : Fragment() {
try {
val dateParts = doc.id.split("-")
val docMonth = dateParts[1].toInt()
val docYear = dateParts[2].toInt()
// Only include records for selected month
if (docMonth == month) {
AttendanceData(
date = doc.id,
checkIn = doc.getString("checkIn") ?: "N/A",
checkOut = doc.getString("checkOut") ?: "N/A"
)
// Only include records for selected month and year
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(
date = doc.id,
checkIns = checkIns,
checkOuts = checkOuts
)
} else null
} else null
} catch (e: Exception) {
Log.e("AttendanceMapping", "Error mapping document: ${doc.id}", e)
null
}
}

View File

@@ -19,6 +19,7 @@ class EmployeeAttendanceListFragment : Fragment() {
private lateinit var adapter: EmployeeStatusListAdapter
private val db = FirebaseFirestore.getInstance()
private var allEmployees = listOf<Employee>() // Store all employees
override fun onCreateView(
inflater: LayoutInflater,
@@ -33,9 +34,35 @@ class EmployeeAttendanceListFragment : Fragment() {
super.onViewCreated(view, savedInstanceState)
setupRecyclerView()
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()) {
adapter.updateEmployees(allEmployees)
return
}
val filteredList = allEmployees.filter {
it.Name.contains(query, ignoreCase = true)
}
adapter.updateEmployees(filteredList)
}
private fun setupRecyclerView() {
adapter = EmployeeStatusListAdapter { employee ->
val bundle = Bundle().apply {
@@ -57,13 +84,19 @@ class EmployeeAttendanceListFragment : Fragment() {
.get()
.addOnSuccessListener { snapshot ->
binding.progressBar.visibility = View.GONE
val employees = snapshot.documents.map { doc ->
Employee(
Id = doc.getString("Id") ?: "Unknown",
Name = doc.getString("Name") ?: "Unknown"
)
allEmployees = snapshot.documents.mapNotNull { doc ->
val status = doc.getBoolean("EmployeeStatus") ?: true // Default to true if field is missing
if (status) {
Employee(
Id = doc.getString("Id") ?: doc.getString("id") ?: "",
Name = doc.getString("Name") ?: doc.getString("name") ?: "",
from = doc.getString("from") ?: ""
)
} else {
null // Exclude employees with EmployeeStatus == false
}
}.sortedBy { it.Name }
adapter.updateEmployees(employees)
adapter.updateEmployees(allEmployees)
}
.addOnFailureListener { e ->
binding.progressBar.visibility = View.GONE

View File

@@ -1,9 +1,12 @@
package `in`.sminnovations.smiadmin.ui.fragments.attendance
import android.R
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Toast
import androidx.fragment.app.Fragment
import com.google.firebase.firestore.FirebaseFirestore
@@ -15,7 +18,7 @@ import `in`.sminnovations.smiadmin.databinding.FragmentEmployeeListBinding
class EmployeeListFragment : Fragment() {
private var _binding: FragmentEmployeeListBinding? = null
private val binding get() = _binding!!
private var employeeList = emptyList<Employee>()
private lateinit var adapter: EmployeeListAdapter
private val db = FirebaseFirestore.getInstance()
@@ -30,11 +33,58 @@ class EmployeeListFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupSpinner()
setupRecyclerView()
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()) {
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() {
adapter = EmployeeListAdapter { employee, newStatus ->
updateEmployeeStatus(employee, newStatus)
@@ -56,13 +106,14 @@ class EmployeeListFragment : Fragment() {
}
snapshot?.let {
val employeeList = snapshot.documents.map { doc ->
employeeList = snapshot.documents.map { doc ->
Employee(
Id = doc.getString("Id") ?: "",
Name = doc.getString("Name") ?: "",
EmployeeStatus = doc.getBoolean("EmployeeStatus") ?: false
Id = doc.getString("Id") ?: doc.getString("id") ?: "",
Name = doc.getString("Name") ?: doc.getString("name") ?: "",
EmployeeStatus = doc.getBoolean("EmployeeStatus") ?: false,
from = doc.getString("from") ?: ""
)
}
}.sortedBy { it.Name?.lowercase() }
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
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.Source
import com.google.firebase.firestore.firestore
import `in`.sminnovations.smiadmin.data.Constant
import `in`.sminnovations.smiadmin.data.Employee
@@ -13,16 +23,38 @@ 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
class ExportTask(private val fragment: Fragment) {
private val db = Firebase.firestore
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
currentExcelTask?.cancel()
// Show progress dialog
val progressDialog = ProgressDialog(fragment.requireContext()).apply {
setMessage("Generating attendance report...")
setCancelable(false)
@@ -31,34 +63,99 @@ class ExportTask(private val fragment: Fragment) {
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") ?: "Unknown",
Name = doc.getString("Name") ?: "Unknown"
Id = doc.getString("Id") ?: doc.getString("id") ?: "",
Name = doc.getString("Name") ?: doc.getString("name") ?: "",
)
}.sortedBy { it.Name }
// Switch to IO dispatcher for file operations
withContext(Dispatchers.IO) {
val pdfGenerator = PDFGenerator(fragment.requireContext(), Constant.COMPANY_NAME)
pdfGenerator.generateAttendanceReport(employees, year, month)
val pdfGenerator = PDFGenerator(fragment.requireContext(), company)
pdfGenerator.generateAttendanceReport(employees, year, month,source)
}.also { pdfFile ->
// Switch back to Main dispatcher for UI updates
withContext(Dispatchers.Main) {
progressDialog.dismiss()
Toast.makeText(
fragment.context,
"Report saved in Downloads folder: ${pdfFile.name}",
Toast.LENGTH_LONG
).show()
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) {
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) {
@@ -78,4 +175,8 @@ class ExportTask(private val fragment: Fragment) {
fun cleanup() {
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
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 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.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.*
import java.util.Calendar
import java.util.Locale
class PDFGenerator(
private val context: Context,
@@ -23,54 +27,69 @@ class PDFGenerator(
private lateinit var currentPage: PdfDocument.Page
private lateinit var canvas: android.graphics.Canvas
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
private val primaryColor = Color.rgb(41, 128, 185) // Blue
private val secondaryColor = Color.rgb(44, 62, 80) // Dark Blue
private val borderColor = Color.rgb(189, 195, 199) // Light Gray
private val backgroundColor = Color.rgb(236, 240, 241) // Very Light Gray
// Constants for page layout
private val pageWidth = 595f // A4 width
private val pageHeight = 842f // A4 height
private val margin = 40f
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(
employees: List<Employee>,
selectedYear: Int,
selectedMonth: Int
selectedMonth: Int,
source: String
): File {
document = PdfDocument()
createNewPage()
// Setup paints
val headerPaint = Paint().apply {
textSize = 28f
textAlign = Paint.Align.CENTER
typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
// Create paint objects
val titlePaint = Paint().apply {
textSize = 32f
typeface = Typeface.create("sans-serif-light", Typeface.BOLD)
color = primaryColor
textAlign = Paint.Align.CENTER
}
val subHeaderPaint = Paint().apply {
textSize = 20f
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 tableHeaderPaint = Paint().apply {
textSize = 14f
textAlign = Paint.Align.LEFT
typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
typeface = Typeface.create("sans-serif-medium", Typeface.BOLD)
color = Color.WHITE
}
val tablePaint = Paint().apply {
val regularPaint = Paint().apply {
textSize = 12f
textAlign = Paint.Align.LEFT
color = secondaryColor
typeface = Typeface.create("sans-serif", Typeface.NORMAL)
color = textColor
}
// Draw headers and table
drawPageHeaders(headerPaint, subHeaderPaint, selectedYear, selectedMonth)
val highlightPaint = Paint().apply {
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)
// Process each employee
@@ -78,7 +97,6 @@ class PDFGenerator(
if (currentY > pageHeight - 100) {
finishCurrentPage()
createNewPage()
drawPageHeaders(headerPaint, subHeaderPaint, selectedYear, selectedMonth)
drawTableHeaders(tableHeaderPaint)
}
@@ -98,58 +116,210 @@ class PDFGenerator(
}
.sorted()
// Draw row background
val rowPaint = Paint().apply {
color = if ((employees.indexOf(employee) % 2) == 0) {
backgroundColor
} else {
Color.WHITE
// Calculate working days (excluding holidays based on company)
val calendar = Calendar.getInstance().apply {
set(selectedYear, selectedMonth - 1, 1)
}
val totalDays = calendar.getActualMaximum(Calendar.DAY_OF_MONTH)
// 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
canvas.drawRect(startX, currentY, pageWidth - startX, currentY + rowHeight, rowPaint)
val workingDays = totalDays - holidays
// Draw employee details
val nameY = currentY + 25f
canvas.drawText(employee.Name, startX + 10f, nameY, tablePaint)
canvas.drawText(
"${presentDates.size} days",
startX + 190f,
nameY,
tablePaint
)
// Format dates in groups of 5
val datesText = presentDates.chunked(5).joinToString("\n") { chunk ->
chunk.joinToString(", ") { day -> String.format("%02d", day) }
// Calculate absent days (excluding holidays)
val absentDays = (1..totalDays).count { day ->
calendar.set(Calendar.DAY_OF_MONTH, day)
val isHoliday = 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
}
!isHoliday && !presentDates.contains(day)
}
// Draw dates in multiple lines if needed
var dateY = nameY
datesText.split("\n").forEach { line ->
canvas.drawText(line, startX + 290f, dateY, tablePaint)
dateY += 15f
}
// Draw row borders
val borderPaint = Paint().apply {
style = Paint.Style.STROKE
strokeWidth = 0.5f
color = borderColor
}
canvas.drawLine(
startX, currentY + rowHeight,
pageWidth - startX, currentY + rowHeight,
borderPaint
)
currentY += rowHeight
drawEmployeeRow(employee, presentDates, absentDays, workingDays, regularPaint, highlightPaint)
}
// Add footer with page numbers
drawFooter(regularPaint)
// Finish and save document
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 file = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
@@ -165,58 +335,18 @@ class PDFGenerator(
}
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)
canvas = currentPage.canvas
currentY = 120f
currentY = margin
}
private fun finishCurrentPage() {
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:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginStart="5dp"
app:layout_constraintStart_toStartOf="parent"
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
android:id="@+id/textViewCount"
android:layout_width="wrap_content"

View File

@@ -4,10 +4,22 @@
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"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerViewEmployees"
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" />
<ProgressBar

View File

@@ -3,11 +3,28 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
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:layout_marginTop="15dp"
android:id="@+id/recyclerViewEmployees"
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" />
<ProgressBar

View File

@@ -76,6 +76,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="8dp"
android:layout_marginBottom="16dp"
app:cardElevation="4dp">
<LinearLayout
@@ -98,6 +99,88 @@
android:text="Download attendance reports" />
</LinearLayout>
</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>
</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: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>

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"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
<androidx.cardview.widget.CardView
android:id="@+id/cardView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="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
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
android:orientation="vertical">
<TextView
android:id="@+id/textViewDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textStyle="bold" />
<LinearLayout
<!-- Header Layout -->
<RelativeLayout
android:id="@+id/headerLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal">
android:padding="16dp"
android:background="?android:attr/selectableItemBackground">
<TextView
android:id="@+id/textViewCheckIn"
android:layout_width="0dp"
android:id="@+id/textViewDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1" />
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:textSize="18sp"
android:textStyle="bold"
/>
<TextView
android:id="@+id/textViewCheckOut"
android:layout_width="0dp"
<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
android:id="@+id/expandableLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone"
android:padding="16dp">
<LinearLayout
android:id="@+id/layoutCheckIns"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1" />
android:orientation="vertical" />
</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"?>
<!-- mobile_navigation.xml -->
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
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
android:id="@+id/navigation_home"
@@ -14,12 +26,24 @@
<action
android:id="@+id/action_home_to_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
android:id="@+id/navigation_settings"
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
android:id="@+id/navigation_employee_list"
@@ -35,6 +59,28 @@
app:destination="@id/navigation_attendance_detail" />
</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
android:id="@+id/navigation_attendance_detail"
android:name="in.sminnovations.smiadmin.ui.fragments.attendance.AttendanceDetailFragment"

View File

@@ -1,6 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path
name="external_downloads"
path="Download" />
<external-files-path
name="external_files"
path="." />
<cache-path
name="shared_qr"
path="." />
</paths>