init commit

This commit is contained in:
chandrashekhar reddy
2024-12-31 16:26:11 +05:30
parent f4bff143da
commit 3e0b496015
25 changed files with 1308 additions and 0 deletions

View File

@@ -0,0 +1,53 @@
package `in`.sminnovations.smiadmin
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.switchmaterial.SwitchMaterial
import `in`.sminnovations.smiadmin.data.Employee
class EmployeeListAdapter(
private val employees: MutableList<Employee> = mutableListOf(),
private val onStatusChanged: (Employee, Boolean) -> Unit
) : RecyclerView.Adapter<EmployeeListAdapter.EmployeeViewHolder>() {
class EmployeeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val nameTextView: TextView = itemView.findViewById(R.id.textViewEmployeeName)
val statusSwitch: SwitchMaterial = itemView.findViewById(R.id.switchStatus)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EmployeeViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_employee, parent, false)
return EmployeeViewHolder(view)
}
override fun onBindViewHolder(holder: EmployeeViewHolder, position: Int) {
val employee = employees[position]
// Remove the previous listener to avoid callbacks while setting up the view
holder.statusSwitch.setOnCheckedChangeListener(null)
// Set the text and switch state
holder.nameTextView.text = employee.Name
holder.statusSwitch.isChecked = employee.EmployeeStatus
// Set up the new listener after setting the checked state
holder.statusSwitch.setOnCheckedChangeListener { buttonView, isChecked ->
if (buttonView.isPressed) { // Only trigger if user actually clicked
employee.EmployeeStatus = isChecked // Update local data
onStatusChanged(employee, isChecked) // Update Firestore
}
}
}
override fun getItemCount() = employees.size
fun updateEmployees(newEmployees: List<Employee>) {
employees.clear()
employees.addAll(newEmployees)
notifyDataSetChanged()
}
}

View File

@@ -0,0 +1,7 @@
package `in`.sminnovations.smiadmin.data
data class AttendanceData(
val date: String = "",
val checkIn: String = "",
val checkOut: String = ""
)

View File

@@ -0,0 +1,7 @@
package `in`.sminnovations.smiadmin.data
data class Employee(
val Id: String = "",
val Name: String = "",
var EmployeeStatus: Boolean = false
)

View File

@@ -0,0 +1,177 @@
package `in`.sminnovations.smiadmin.ui
import android.content.ContentValues
import android.content.Context
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import com.google.firebase.Firebase
import com.google.firebase.firestore.firestore
import `in`.sminnovations.smiadmin.data.AttendanceData
import `in`.sminnovations.smiadmin.data.Employee
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.tasks.await
import kotlinx.coroutines.withContext
import java.io.File
import java.io.IOException
class CSVGenerator(private val context: Context) {
suspend fun generateAttendanceReport(
employees: List<Employee>,
selectedYear: Int,
selectedMonth: Int
): File {
val fileName = "Attendance_${selectedYear}_${selectedMonth}.csv"
return withContext(Dispatchers.IO) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
put(MediaStore.MediaColumns.MIME_TYPE, "text/csv")
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
}
val uri = context.contentResolver.insert(
MediaStore.Downloads.EXTERNAL_CONTENT_URI,
contentValues
) ?: throw IOException("Failed to create new MediaStore record.")
context.contentResolver.openOutputStream(uri)?.use { outputStream ->
val writer = outputStream.bufferedWriter()
val dates = mutableSetOf<String>()
val attendanceData = mutableMapOf<String, Map<String, AttendanceData>>()
// Format month with leading zero
val monthStr = String.format("%02d", selectedMonth)
// Collect attendance data for each employee
employees.forEach { employee ->
// Get all documents and filter by month pattern in document ID
val snapshot = Firebase.firestore
.collection("attendance_${employee.Id}")
.get()
.await()
val employeeAttendance = snapshot.documents
.filter { doc ->
// Filter documents that match the pattern DD-MM-YYYY for selected month
doc.id.matches(Regex("\\d{2}-$monthStr-$selectedYear"))
}
.associate { doc ->
dates.add(doc.id)
doc.id to AttendanceData(
date = doc.id,
checkIn = doc.getString("checkIn") ?: "N/A",
checkOut = doc.getString("checkOut") ?: "N/A"
)
}
attendanceData[employee.Id] = employeeAttendance
}
// Sort dates
val sortedDates = dates.sortedBy { date ->
val parts = date.split("-")
val day = parts[0].toInt()
String.format("%02d", day)
}
// Write header
val headerRow = listOf("Employee ID", "Name") + sortedDates
writer.write(headerRow.joinToString(","))
writer.newLine()
// Write employee data
employees.forEach { employee ->
val rowData = mutableListOf<String>()
rowData.add(employee.Id)
rowData.add(employee.Name)
val employeeAttendance = attendanceData[employee.Id] ?: emptyMap()
sortedDates.forEach { date ->
val attendance = employeeAttendance[date]
val cellValue = when {
attendance == null -> "Absent"
attendance.checkOut == "N/A" -> attendance.checkIn
else -> "${attendance.checkIn} - ${attendance.checkOut}"
}
rowData.add("\"${cellValue.replace("\"", "\"\"")}\"")
}
writer.write(rowData.joinToString(","))
writer.newLine()
}
writer.flush()
}
File(Environment.getExternalStorageDirectory(), "Download/$fileName")
} else {
val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
val file = File(downloadsDir, fileName)
file.bufferedWriter().use { writer ->
val dates = mutableSetOf<String>()
val attendanceData = mutableMapOf<String, Map<String, AttendanceData>>()
val monthStr = String.format("%02d", selectedMonth)
employees.forEach { employee ->
val snapshot = Firebase.firestore
.collection("attendance_${employee.Id}")
.get()
.await()
val employeeAttendance = snapshot.documents
.filter { doc ->
doc.id.matches(Regex("\\d{2}-$monthStr-$selectedYear"))
}
.associate { doc ->
dates.add(doc.id)
doc.id to AttendanceData(
date = doc.id,
checkIn = doc.getString("checkIn") ?: "N/A",
checkOut = doc.getString("checkOut") ?: "N/A"
)
}
attendanceData[employee.Id] = employeeAttendance
}
val sortedDates = dates.sortedBy { date ->
val parts = date.split("-")
val day = parts[0].toInt()
String.format("%02d", day)
}
val headerRow = listOf("Employee ID", "Name") + sortedDates
writer.write(headerRow.joinToString(","))
writer.newLine()
employees.forEach { employee ->
val rowData = mutableListOf<String>()
rowData.add(employee.Id)
rowData.add(employee.Name)
val employeeAttendance = attendanceData[employee.Id] ?: emptyMap()
sortedDates.forEach { date ->
val attendance = employeeAttendance[date]
val cellValue = when {
attendance == null -> "Absent"
attendance.checkOut == "N/A" -> attendance.checkIn
else -> "${attendance.checkIn} - ${attendance.checkOut}"
}
rowData.add("\"${cellValue.replace("\"", "\"\"")}\"")
}
writer.write(rowData.joinToString(","))
writer.newLine()
}
writer.flush()
}
file
}
}
}
}

View File

@@ -0,0 +1,97 @@
package `in`.sminnovations.smiadmin.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment
import com.google.firebase.firestore.FirebaseFirestore
import `in`.sminnovations.smiadmin.EmployeeListAdapter
import `in`.sminnovations.smiadmin.data.Employee
import `in`.sminnovations.smiadmin.databinding.FragmentEmployeeListBinding
class EmployeeListFragment : Fragment() {
private var _binding: FragmentEmployeeListBinding? = null
private val binding get() = _binding!!
private lateinit var adapter: EmployeeListAdapter
private val db = FirebaseFirestore.getInstance()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentEmployeeListBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupRecyclerView()
fetchEmployees()
}
private fun setupRecyclerView() {
adapter = EmployeeListAdapter { employee, newStatus ->
updateEmployeeStatus(employee, newStatus)
}
binding.recyclerViewEmployees.adapter = adapter
}
private fun fetchEmployees() {
binding.progressBar.visibility = View.VISIBLE
db.collection("employees")
.addSnapshotListener { snapshot, error ->
binding.progressBar.visibility = View.GONE
if (error != null) {
// Handle error
Toast.makeText(context, "Error: ${error.message}", Toast.LENGTH_SHORT).show()
return@addSnapshotListener
}
snapshot?.let {
val employeeList = snapshot.documents.map { doc ->
Employee(
Id = doc.getString("Id") ?: "",
Name = doc.getString("Name") ?: "",
EmployeeStatus = doc.getBoolean("EmployeeStatus") ?: false
)
}
adapter.updateEmployees(employeeList)
}
}
}
private fun updateEmployeeStatus(employee: Employee, newStatus: Boolean) {
val wasEnabled = employee.EmployeeStatus
// Optimistically update the UI
employee.EmployeeStatus = newStatus
adapter.notifyDataSetChanged()
db.collection("employees")
.document(employee.Id)
.update("EmployeeStatus", newStatus)
.addOnSuccessListener {
// Update was successful
Toast.makeText(context, "Status updated successfully", Toast.LENGTH_SHORT).show()
}
.addOnFailureListener { e ->
// Revert the change on failure
employee.EmployeeStatus = wasEnabled
adapter.notifyDataSetChanged()
Toast.makeText(context, "Error updating status: ${e.message}", Toast.LENGTH_SHORT).show()
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@@ -0,0 +1,170 @@
package `in`.sminnovations.smiadmin.ui
import android.Manifest
import android.app.ProgressDialog
import android.content.ActivityNotFoundException
import android.content.ContentUris
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.provider.MediaStore
import android.provider.Settings
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.cardview.widget.CardView
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.google.firebase.Firebase
import com.google.firebase.firestore.firestore
import `in`.sminnovations.smiadmin.R
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
class HomeFragment : Fragment() {
private val db = Firebase.firestore
private val STORAGE_PERMISSION_CODE = 101
private var currentExcelTask: Job? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_home, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.findViewById<CardView>(R.id.cardEmployeeStatus).setOnClickListener {
findNavController().navigate(R.id.action_home_to_employee_list)
}
view.findViewById<CardView>(R.id.cardAttendanceReport).setOnClickListener {
findNavController().navigate(R.id.action_home_to_employee_attendance_list)
}
view.findViewById<CardView>(R.id.cardDownloadReport).setOnClickListener {
if (checkStoragePermission()) {
showMonthYearPicker()
} else {
requestStoragePermission()
}
}
}
private fun showMonthYearPicker() {
MonthYearPickerDialog().apply {
setOnDateSelectedListener { year, month ->
downloadAttendanceReport(year, month)
}
}.show(parentFragmentManager, "MonthYearPicker")
}
private fun downloadAttendanceReport(year: Int, month: Int) {
currentExcelTask?.cancel()
val progressDialog = ProgressDialog(requireContext()).apply {
setMessage("Generating report...")
setCancelable(false)
show()
}
currentExcelTask = lifecycleScope.launch {
try {
// First get all employees
val employeesSnapshot = withContext(Dispatchers.IO) {
db.collection("employees").get().await()
}
val employees = employeesSnapshot.documents.map { doc ->
Employee(
Id = doc.getString("Id") ?: "Unknown",
Name = doc.getString("Name") ?: "Unknown"
)
}.sortedBy { it.Name }
// Generate CSV file
val csvGenerator = CSVGenerator(requireContext())
val csvFile = csvGenerator.generateAttendanceReport(employees, year, month)
progressDialog.dismiss()
// Show success message with file location
Toast.makeText(
context,
"Report saved in Downloads folder: ${csvFile.name}",
Toast.LENGTH_LONG
).show()
} catch (e: Exception) {
progressDialog.dismiss()
Toast.makeText(context, "Error: ${e.message}", Toast.LENGTH_LONG).show()
}
}
}
private fun checkStoragePermission(): Boolean {
return if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
ContextCompat.checkSelfPermission(
requireContext(),
Manifest.permission.WRITE_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED
} else {
Environment.isExternalStorageManager()
}
}
private fun requestStoragePermission() {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
requestPermissions(
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
STORAGE_PERMISSION_CODE
)
} else {
try {
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
val uri = Uri.fromParts("package", requireContext().packageName, null)
intent.data = uri
startActivity(intent)
} catch (e: Exception) {
val intent = Intent()
intent.action = Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION
startActivity(intent)
}
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == STORAGE_PERMISSION_CODE) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// Permission granted, now show the picker
showMonthYearPicker()
} else {
Toast.makeText(
requireContext(),
"Storage permission is required to download reports",
Toast.LENGTH_SHORT
).show()
}
}
}
}

View File

@@ -0,0 +1,41 @@
package `in`.sminnovations.smiadmin.ui
import android.app.DatePickerDialog
import android.app.Dialog
import android.content.res.Resources
import android.os.Bundle
import android.view.View
import androidx.fragment.app.DialogFragment
import `in`.sminnovations.smiadmin.R
import java.util.Calendar
class MonthYearPickerDialog : DialogFragment() {
private var onDateSelected: ((Int, Int) -> Unit)? = null
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val calendar = Calendar.getInstance()
val year = calendar.get(Calendar.YEAR)
val month = calendar.get(Calendar.MONTH)
val dialog = DatePickerDialog(
requireContext(),
R.style.DialogTheme,
{ _, selectedYear, selectedMonth, _ ->
onDateSelected?.invoke(selectedYear, selectedMonth + 1)
},
year,
month,
1
)
// Hide the day picker as we only need month and year
val datePicker = dialog.datePicker
datePicker.findViewById<View>(Resources.getSystem().getIdentifier("day", "id", "android"))?.visibility = View.GONE
return dialog
}
fun setOnDateSelectedListener(listener: (Int, Int) -> Unit) {
onDateSelected = listener
}
}

View File

@@ -0,0 +1,37 @@
package `in`.sminnovations.smiadmin.ui
import android.content.pm.PackageManager
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import `in`.sminnovations.smiadmin.R
class SettingsFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val root = inflater.inflate(R.layout.fragment_settings, container, false)
// Set app version
val versionTextView: TextView = root.findViewById(R.id.textViewVersion)
try {
val pInfo = requireContext().packageManager.getPackageInfo(
requireContext().packageName, 0
)
val version = pInfo.versionName
val verCode = pInfo.versionCode
versionTextView.text = version + " (" + verCode +")"
} catch (e: PackageManager.NameNotFoundException) {
e.printStackTrace()
}
return root
}
}

View File

@@ -0,0 +1,49 @@
package `in`.sminnovations.smiadmin.ui.attendance
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import `in`.sminnovations.smiadmin.R
import `in`.sminnovations.smiadmin.data.AttendanceData
class AttendanceAdapter : RecyclerView.Adapter<AttendanceAdapter.ViewHolder>() {
private val attendanceList = mutableListOf<AttendanceData>()
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)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_attendance, parent, false)
return ViewHolder(view)
}
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}"
}
override fun getItemCount() = attendanceList.size
// In AttendanceAdapter.kt
fun updateAttendance(newAttendance: List<AttendanceData>) {
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()
}
}

View File

@@ -0,0 +1,131 @@
package `in`.sminnovations.smiadmin.ui.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
import `in`.sminnovations.smiadmin.data.AttendanceData
import `in`.sminnovations.smiadmin.databinding.FragmentAttendanceDetailBinding
import java.util.Calendar
class AttendanceDetailFragment : Fragment() {
private var _binding: FragmentAttendanceDetailBinding? = null
private val binding get() = _binding!!
private lateinit var adapter: AttendanceAdapter
private val db = FirebaseFirestore.getInstance()
private val months = arrayOf("January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December")
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentAttendanceDetailBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val employeeId = arguments?.getString("employeeId") ?: return
val employeeName = arguments?.getString("employeeName") ?: "Unknown"
binding.textViewEmployeeName.text = employeeName
setupMonthSpinner()
setupRecyclerView()
// Get current month (1-12)
val currentMonth = Calendar.getInstance().get(Calendar.MONTH) + 1
fetchAttendanceData(employeeId, currentMonth)
}
private fun setupMonthSpinner() {
val adapter = ArrayAdapter(
requireContext(),
R.layout.simple_spinner_item,
months
)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
binding.spinnerMonth.adapter = adapter
// Set current month as default
binding.spinnerMonth.setSelection(Calendar.getInstance().get(Calendar.MONTH))
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)
}
override fun onNothingSelected(parent: AdapterView<*>?) {}
}
}
private fun setupRecyclerView() {
adapter = AttendanceAdapter()
binding.recyclerViewAttendance.adapter = adapter
}
private fun fetchAttendanceData(employeeId: String, month: Int) {
binding.progressBar.visibility = View.VISIBLE
db.collection("attendance_$employeeId")
.get()
.addOnSuccessListener { snapshot ->
binding.progressBar.visibility = View.GONE
val attendanceList = snapshot.documents.mapNotNull { doc ->
try {
val dateParts = doc.id.split("-")
val docMonth = dateParts[1].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"
)
} else null
} catch (e: Exception) {
null
}
}
// Sort dates in descending order (newest first)
val sortedList = attendanceList.sortedByDescending {
try {
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)
} catch (e: Exception) {
"00000000"
}
}
// Update count
binding.textViewCount.text = "Total Records: ${sortedList.size}"
adapter.updateAttendance(sortedList)
}
.addOnFailureListener { e ->
binding.progressBar.visibility = View.GONE
Toast.makeText(context, "Error: ${e.message}", Toast.LENGTH_SHORT).show()
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@@ -0,0 +1,77 @@
package `in`.sminnovations.smiadmin.ui.attendance
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.firebase.firestore.FirebaseFirestore
import `in`.sminnovations.smiadmin.R
import `in`.sminnovations.smiadmin.data.Employee
import `in`.sminnovations.smiadmin.databinding.FragmentEmployeeAttendanceListBinding
class EmployeeAttendanceListFragment : Fragment() {
private var _binding: FragmentEmployeeAttendanceListBinding? = null
private val binding get() = _binding!!
private lateinit var adapter: EmployeeListAdapter
private val db = FirebaseFirestore.getInstance()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentEmployeeAttendanceListBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupRecyclerView()
fetchEmployees()
}
private fun setupRecyclerView() {
adapter = EmployeeListAdapter { employee ->
val bundle = Bundle().apply {
putString("employeeId", employee.Id)
putString("employeeName", employee.Name)
}
findNavController().navigate(
R.id.action_employee_list_to_attendance_detail,
bundle
)
}
binding.recyclerViewEmployees.adapter = adapter
}
private fun fetchEmployees() {
binding.progressBar.visibility = View.VISIBLE
db.collection("employees")
.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"
)
}.sortedBy { it.Name }
adapter.updateEmployees(employees)
}
.addOnFailureListener { e ->
binding.progressBar.visibility = View.GONE
Toast.makeText(context, "Error: ${e.message}", Toast.LENGTH_SHORT).show()
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@@ -0,0 +1,42 @@
package `in`.sminnovations.smiadmin.ui.attendance
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import `in`.sminnovations.smiadmin.R
import `in`.sminnovations.smiadmin.data.Employee
class EmployeeListAdapter(
private val onEmployeeClick: (Employee) -> Unit
) : RecyclerView.Adapter<EmployeeListAdapter.ViewHolder>() {
private val employees = mutableListOf<Employee>()
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val nameTextView: TextView = itemView.findViewById(R.id.textViewEmployeeName)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_employee_simple, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val employee = employees[position]
holder.nameTextView.text = employee.Name
holder.itemView.setOnClickListener {
onEmployeeClick(employee)
}
}
override fun getItemCount() = employees.size
fun updateEmployees(newEmployees: List<Employee>) {
employees.clear()
employees.addAll(newEmployees)
notifyDataSetChanged()
}
}

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="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z"/>
</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="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z"/>
</vector>

View File

@@ -0,0 +1,56 @@
<?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">
<TextView
android:id="@+id/textViewEmployeeName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintTop_toTopOf="parent" />
<Spinner
android:id="@+id/spinnerMonth"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/textViewEmployeeName" />
<TextView
android:id="@+id/textViewCount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:textStyle="bold"
android:textSize="14sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBaseline_toBaselineOf="@id/spinnerMonth"
tools:text="Total Records: 0" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerViewAttendance"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="8dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/spinnerMonth" />
<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" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,23 @@
<?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"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerViewEmployees"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
<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" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,23 @@
<?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"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerViewEmployees"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
<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" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,103 @@
<?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:orientation="vertical"
android:padding="16dp">
<!-- Employee Status Card -->
<androidx.cardview.widget.CardView
android:id="@+id/cardEmployeeStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
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="Employee Status"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="View current employee status" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- Attendance Report Card -->
<androidx.cardview.widget.CardView
android:id="@+id/cardAttendanceReport"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
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="Attendance Report"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="View attendance reports" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- Download Report Card -->
<androidx.cardview.widget.CardView
android:id="@+id/cardDownloadReport"
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="Download 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 attendance reports" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
</ScrollView>

View File

@@ -0,0 +1,50 @@
<?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="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Settings"
android:textSize="24sp"
android:textStyle="bold"
android:layout_marginBottom="24dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="App Details"
android:textSize="18sp"
android:textStyle="bold"
android:layout_marginBottom="8dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Version:"
android:textStyle="bold"
android:layout_marginTop="16dp" />
<TextView
android:id="@+id/textViewVersion"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="About:"
android:textStyle="bold"
android:layout_marginTop="16dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="SMI Admin App(Employee Attendance)"
android:layout_marginTop="4dp" />
</LinearLayout>

View File

@@ -0,0 +1,42 @@
<?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"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
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:id="@+id/textViewDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textStyle="bold" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal">
<TextView
android:id="@+id/textViewCheckIn"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<TextView
android:id="@+id/textViewCheckOut"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>

View File

@@ -0,0 +1,30 @@
<?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"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:cardCornerRadius="8dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp">
<TextView
android:id="@+id/textViewEmployeeName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="16sp" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switchStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</androidx.cardview.widget.CardView>

View File

@@ -0,0 +1,17 @@
<?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"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:cardCornerRadius="8dp"
app:cardElevation="4dp">
<TextView
android:id="@+id/textViewEmployeeName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:textSize="16sp" />
</androidx.cardview.widget.CardView>

View File

@@ -0,0 +1,10 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/navigation_home"
android:icon="@drawable/ic_home"
android:title="Home" />
<item
android:id="@+id/navigation_settings"
android:icon="@drawable/ic_settings"
android:title="Settings" />
</menu>

View File

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<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">
<fragment
android:id="@+id/navigation_home"
android:name="in.sminnovations.smiadmin.ui.HomeFragment"
android:label="Home">
<action
android:id="@+id/action_home_to_employee_list"
app:destination="@id/navigation_employee_list" />
<action
android:id="@+id/action_home_to_employee_attendance_list"
app:destination="@id/navigation_employee_attendance_list" />
</fragment>
<fragment
android:id="@+id/navigation_settings"
android:name="in.sminnovations.smiadmin.ui.SettingsFragment"
android:label="Settings" />
<fragment
android:id="@+id/navigation_employee_list"
android:name="in.sminnovations.smiadmin.ui.EmployeeListFragment"
android:label="Employee List" />
<fragment
android:id="@+id/navigation_employee_attendance_list"
android:name="in.sminnovations.smiadmin.ui.attendance.EmployeeAttendanceListFragment"
android:label="Employee List">
<action
android:id="@+id/action_employee_list_to_attendance_detail"
app:destination="@id/navigation_attendance_detail" />
</fragment>
<fragment
android:id="@+id/navigation_attendance_detail"
android:name="in.sminnovations.smiadmin.ui.attendance.AttendanceDetailFragment"
android:label="Attendance Detail">
<argument
android:name="employeeId"
app:argType="string" />
<argument
android:name="employeeName"
app:argType="string" />
</fragment>
</navigation>

View File

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