Merge remote-tracking branch 'origin/main'

# Conflicts:
#	.idea/other.xml
#	app/src/main/java/in/sminnovations/smiadmin/data/Employee.kt
This commit is contained in:
sathwikcs
2025-03-03 13:09:08 +05:30
12 changed files with 402 additions and 61 deletions

View File

@@ -13,8 +13,8 @@ android {
applicationId "in.sminnovations.smiadmin"
minSdk 26
targetSdk 34
versionCode 4
versionName "1.4"
versionCode 5
versionName "1.5"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

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

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

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

@@ -83,7 +83,7 @@ class AddEmployeeFragment : Fragment() {
}
private fun generateEmployeeId(name: String, phone: String): String {
val namePrefix = name.take(4).uppercase()
val namePrefix = name.take(4).lowercase()
val phoneSuffix = phone.takeLast(4)
return "$namePrefix$phoneSuffix"
}
@@ -136,16 +136,29 @@ class AddEmployeeFragment : Fragment() {
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 = 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")

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
@@ -123,13 +124,38 @@ class AttendanceDetailFragment : Fragment() {
// Only include records for selected month and year
if (docMonth == month && docYear == year) {
AttendanceData(
date = doc.id,
checkIn = doc.getString("checkIn") ?: "N/A",
checkOut = doc.getString("checkOut") ?: "N/A"
)
// 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

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

@@ -18,7 +18,7 @@
android:id="@+id/spinnerMonth"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginStart="5dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/textViewEmployeeName" />
@@ -26,7 +26,7 @@
android:id="@+id/spinnerYear"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginStart="5dp"
app:layout_constraintStart_toEndOf="@id/spinnerMonth"
app:layout_constraintTop_toTopOf="@id/spinnerMonth" />

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>