diff --git a/app/build.gradle b/app/build.gradle index 70a777e..68fa34a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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" } diff --git a/app/src/main/java/in/sminnovations/smiadmin/BaseActivity.kt b/app/src/main/java/in/sminnovations/smiadmin/BaseActivity.kt new file mode 100644 index 0000000..98eac9f --- /dev/null +++ b/app/src/main/java/in/sminnovations/smiadmin/BaseActivity.kt @@ -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() + } +} \ No newline at end of file diff --git a/app/src/main/java/in/sminnovations/smiadmin/MainActivity.kt b/app/src/main/java/in/sminnovations/smiadmin/MainActivity.kt index 81ca4e8..0ae23ce 100644 --- a/app/src/main/java/in/sminnovations/smiadmin/MainActivity.kt +++ b/app/src/main/java/in/sminnovations/smiadmin/MainActivity.kt @@ -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 diff --git a/app/src/main/java/in/sminnovations/smiadmin/data/AttendanceData.kt b/app/src/main/java/in/sminnovations/smiadmin/data/AttendanceData.kt index c62a693..d1b7f52 100644 --- a/app/src/main/java/in/sminnovations/smiadmin/data/AttendanceData.kt +++ b/app/src/main/java/in/sminnovations/smiadmin/data/AttendanceData.kt @@ -2,6 +2,7 @@ package `in`.sminnovations.smiadmin.data data class AttendanceData( val date: String = "", - val checkIn: String = "", - val checkOut: String = "" + val checkIns: Map, + val checkOuts: Map, + var isExpanded: Boolean = false ) diff --git a/app/src/main/java/in/sminnovations/smiadmin/ui/adapter/AttendanceAdapter.kt b/app/src/main/java/in/sminnovations/smiadmin/ui/adapter/AttendanceAdapter.kt index 90ac96a..62a09df 100644 --- a/app/src/main/java/in/sminnovations/smiadmin/ui/adapter/AttendanceAdapter.kt +++ b/app/src/main/java/in/sminnovations/smiadmin/ui/adapter/AttendanceAdapter.kt @@ -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() { 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() { 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) { + 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(R.id.textViewCheckIn) + val checkOutView = pairLayout.findViewById(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, + private val newList: List +) : 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) } \ No newline at end of file diff --git a/app/src/main/java/in/sminnovations/smiadmin/ui/fragments/AddEmployeeFragment.kt b/app/src/main/java/in/sminnovations/smiadmin/ui/fragments/AddEmployeeFragment.kt index 70607a0..da517aa 100644 --- a/app/src/main/java/in/sminnovations/smiadmin/ui/fragments/AddEmployeeFragment.kt +++ b/app/src/main/java/in/sminnovations/smiadmin/ui/fragments/AddEmployeeFragment.kt @@ -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") diff --git a/app/src/main/java/in/sminnovations/smiadmin/ui/fragments/attendance/AttendanceDetailFragment.kt b/app/src/main/java/in/sminnovations/smiadmin/ui/fragments/attendance/AttendanceDetailFragment.kt index 4cd1fb8..435af74 100644 --- a/app/src/main/java/in/sminnovations/smiadmin/ui/fragments/attendance/AttendanceDetailFragment.kt +++ b/app/src/main/java/in/sminnovations/smiadmin/ui/fragments/attendance/AttendanceDetailFragment.kt @@ -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() + val checkOuts = mutableMapOf() + + // 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 } } diff --git a/app/src/main/java/in/sminnovations/smiadmin/ui/utils/NetworkUtils.kt b/app/src/main/java/in/sminnovations/smiadmin/ui/utils/NetworkUtils.kt new file mode 100644 index 0000000..cb9fa50 --- /dev/null +++ b/app/src/main/java/in/sminnovations/smiadmin/ui/utils/NetworkUtils.kt @@ -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 + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/baseline_arrow_drop_down_24.xml b/app/src/main/res/drawable/baseline_arrow_drop_down_24.xml new file mode 100644 index 0000000..a77e331 --- /dev/null +++ b/app/src/main/res/drawable/baseline_arrow_drop_down_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/layout/fragment_attendance_detail.xml b/app/src/main/res/layout/fragment_attendance_detail.xml index db08bfb..3fe65ca 100644 --- a/app/src/main/res/layout/fragment_attendance_detail.xml +++ b/app/src/main/res/layout/fragment_attendance_detail.xml @@ -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" /> diff --git a/app/src/main/res/layout/item_attendance.xml b/app/src/main/res/layout/item_attendance.xml index 626dbaa..f91915f 100644 --- a/app/src/main/res/layout/item_attendance.xml +++ b/app/src/main/res/layout/item_attendance.xml @@ -1,42 +1,71 @@ - + 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"> + android:orientation="vertical"> - - - + + android:padding="16dp" + android:background="?android:attr/selectableItemBackground"> + android:layout_alignParentStart="true" + android:layout_centerVertical="true" + android:textSize="18sp" + android:textStyle="bold" + /> - + + + + + + + + + + android:orientation="vertical" /> + - + \ No newline at end of file diff --git a/app/src/main/res/layout/item_checkin_checkout_pair.xml b/app/src/main/res/layout/item_checkin_checkout_pair.xml new file mode 100644 index 0000000..420993a --- /dev/null +++ b/app/src/main/res/layout/item_checkin_checkout_pair.xml @@ -0,0 +1,21 @@ + + + + + + + + \ No newline at end of file