From 3e0b4960150fa73874ea89e9e625de2a26d73dc8 Mon Sep 17 00:00:00 2001 From: chandrashekhar reddy Date: Tue, 31 Dec 2024 16:26:11 +0530 Subject: [PATCH] init commit --- .../smiadmin/EmployeeListAdapter.kt | 53 ++++++ .../smiadmin/data/AttendanceData.kt | 7 + .../sminnovations/smiadmin/data/Employee.kt | 7 + .../sminnovations/smiadmin/ui/CSVGenerator.kt | 177 ++++++++++++++++++ .../smiadmin/ui/EmployeeListFragment.kt | 97 ++++++++++ .../sminnovations/smiadmin/ui/HomeFragment.kt | 170 +++++++++++++++++ .../smiadmin/ui/MonthYearPickerDialog.kt | 41 ++++ .../smiadmin/ui/SettingsFragment.kt | 37 ++++ .../ui/attendance/AttendanceAdapter.kt | 49 +++++ .../ui/attendance/AttendanceDetailFragment.kt | 131 +++++++++++++ .../EmployeeAttendanceListFragment.kt | 77 ++++++++ .../ui/attendance/EmployeeListAdapter.kt | 42 +++++ app/src/main/res/drawable/ic_home.xml | 5 + app/src/main/res/drawable/ic_settings.xml | 5 + .../res/layout/fragment_attendance_detail.xml | 56 ++++++ .../fragment_employee_attendance_list.xml | 23 +++ .../res/layout/fragment_employee_list.xml | 23 +++ app/src/main/res/layout/fragment_home.xml | 103 ++++++++++ app/src/main/res/layout/fragment_settings.xml | 50 +++++ app/src/main/res/layout/item_attendance.xml | 42 +++++ app/src/main/res/layout/item_employee.xml | 30 +++ .../main/res/layout/item_employee_simple.xml | 17 ++ app/src/main/res/menu/bottom_nav_menu.xml | 10 + .../main/res/navigation/mobile_navigation.xml | 50 +++++ app/src/main/res/xml/provider_paths.xml | 6 + 25 files changed, 1308 insertions(+) create mode 100644 app/src/main/java/in/sminnovations/smiadmin/EmployeeListAdapter.kt create mode 100644 app/src/main/java/in/sminnovations/smiadmin/data/AttendanceData.kt create mode 100644 app/src/main/java/in/sminnovations/smiadmin/data/Employee.kt create mode 100644 app/src/main/java/in/sminnovations/smiadmin/ui/CSVGenerator.kt create mode 100644 app/src/main/java/in/sminnovations/smiadmin/ui/EmployeeListFragment.kt create mode 100644 app/src/main/java/in/sminnovations/smiadmin/ui/HomeFragment.kt create mode 100644 app/src/main/java/in/sminnovations/smiadmin/ui/MonthYearPickerDialog.kt create mode 100644 app/src/main/java/in/sminnovations/smiadmin/ui/SettingsFragment.kt create mode 100644 app/src/main/java/in/sminnovations/smiadmin/ui/attendance/AttendanceAdapter.kt create mode 100644 app/src/main/java/in/sminnovations/smiadmin/ui/attendance/AttendanceDetailFragment.kt create mode 100644 app/src/main/java/in/sminnovations/smiadmin/ui/attendance/EmployeeAttendanceListFragment.kt create mode 100644 app/src/main/java/in/sminnovations/smiadmin/ui/attendance/EmployeeListAdapter.kt create mode 100644 app/src/main/res/drawable/ic_home.xml create mode 100644 app/src/main/res/drawable/ic_settings.xml create mode 100644 app/src/main/res/layout/fragment_attendance_detail.xml create mode 100644 app/src/main/res/layout/fragment_employee_attendance_list.xml create mode 100644 app/src/main/res/layout/fragment_employee_list.xml create mode 100644 app/src/main/res/layout/fragment_home.xml create mode 100644 app/src/main/res/layout/fragment_settings.xml create mode 100644 app/src/main/res/layout/item_attendance.xml create mode 100644 app/src/main/res/layout/item_employee.xml create mode 100644 app/src/main/res/layout/item_employee_simple.xml create mode 100644 app/src/main/res/menu/bottom_nav_menu.xml create mode 100644 app/src/main/res/navigation/mobile_navigation.xml create mode 100644 app/src/main/res/xml/provider_paths.xml diff --git a/app/src/main/java/in/sminnovations/smiadmin/EmployeeListAdapter.kt b/app/src/main/java/in/sminnovations/smiadmin/EmployeeListAdapter.kt new file mode 100644 index 0000000..167ebf4 --- /dev/null +++ b/app/src/main/java/in/sminnovations/smiadmin/EmployeeListAdapter.kt @@ -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 = mutableListOf(), + private val onStatusChanged: (Employee, Boolean) -> Unit +) : RecyclerView.Adapter() { + + 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) { + employees.clear() + employees.addAll(newEmployees) + notifyDataSetChanged() + } +} diff --git a/app/src/main/java/in/sminnovations/smiadmin/data/AttendanceData.kt b/app/src/main/java/in/sminnovations/smiadmin/data/AttendanceData.kt new file mode 100644 index 0000000..c62a693 --- /dev/null +++ b/app/src/main/java/in/sminnovations/smiadmin/data/AttendanceData.kt @@ -0,0 +1,7 @@ +package `in`.sminnovations.smiadmin.data + +data class AttendanceData( + val date: String = "", + val checkIn: String = "", + val checkOut: String = "" +) diff --git a/app/src/main/java/in/sminnovations/smiadmin/data/Employee.kt b/app/src/main/java/in/sminnovations/smiadmin/data/Employee.kt new file mode 100644 index 0000000..8bdaf7d --- /dev/null +++ b/app/src/main/java/in/sminnovations/smiadmin/data/Employee.kt @@ -0,0 +1,7 @@ +package `in`.sminnovations.smiadmin.data + +data class Employee( + val Id: String = "", + val Name: String = "", + var EmployeeStatus: Boolean = false +) diff --git a/app/src/main/java/in/sminnovations/smiadmin/ui/CSVGenerator.kt b/app/src/main/java/in/sminnovations/smiadmin/ui/CSVGenerator.kt new file mode 100644 index 0000000..f550d34 --- /dev/null +++ b/app/src/main/java/in/sminnovations/smiadmin/ui/CSVGenerator.kt @@ -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, + 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() + val attendanceData = mutableMapOf>() + + // 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() + 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() + val attendanceData = mutableMapOf>() + + 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() + 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 + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/in/sminnovations/smiadmin/ui/EmployeeListFragment.kt b/app/src/main/java/in/sminnovations/smiadmin/ui/EmployeeListFragment.kt new file mode 100644 index 0000000..5352801 --- /dev/null +++ b/app/src/main/java/in/sminnovations/smiadmin/ui/EmployeeListFragment.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/in/sminnovations/smiadmin/ui/HomeFragment.kt b/app/src/main/java/in/sminnovations/smiadmin/ui/HomeFragment.kt new file mode 100644 index 0000000..56c5d37 --- /dev/null +++ b/app/src/main/java/in/sminnovations/smiadmin/ui/HomeFragment.kt @@ -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(R.id.cardEmployeeStatus).setOnClickListener { + findNavController().navigate(R.id.action_home_to_employee_list) + } + + view.findViewById(R.id.cardAttendanceReport).setOnClickListener { + findNavController().navigate(R.id.action_home_to_employee_attendance_list) + } + + view.findViewById(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, + 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() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/in/sminnovations/smiadmin/ui/MonthYearPickerDialog.kt b/app/src/main/java/in/sminnovations/smiadmin/ui/MonthYearPickerDialog.kt new file mode 100644 index 0000000..78fa43e --- /dev/null +++ b/app/src/main/java/in/sminnovations/smiadmin/ui/MonthYearPickerDialog.kt @@ -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(Resources.getSystem().getIdentifier("day", "id", "android"))?.visibility = View.GONE + + return dialog + } + + fun setOnDateSelectedListener(listener: (Int, Int) -> Unit) { + onDateSelected = listener + } +} diff --git a/app/src/main/java/in/sminnovations/smiadmin/ui/SettingsFragment.kt b/app/src/main/java/in/sminnovations/smiadmin/ui/SettingsFragment.kt new file mode 100644 index 0000000..8062596 --- /dev/null +++ b/app/src/main/java/in/sminnovations/smiadmin/ui/SettingsFragment.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/in/sminnovations/smiadmin/ui/attendance/AttendanceAdapter.kt b/app/src/main/java/in/sminnovations/smiadmin/ui/attendance/AttendanceAdapter.kt new file mode 100644 index 0000000..0fbbaaa --- /dev/null +++ b/app/src/main/java/in/sminnovations/smiadmin/ui/attendance/AttendanceAdapter.kt @@ -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() { + private val attendanceList = mutableListOf() + + 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) { + 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() + } +} \ No newline at end of file diff --git a/app/src/main/java/in/sminnovations/smiadmin/ui/attendance/AttendanceDetailFragment.kt b/app/src/main/java/in/sminnovations/smiadmin/ui/attendance/AttendanceDetailFragment.kt new file mode 100644 index 0000000..9ff98c3 --- /dev/null +++ b/app/src/main/java/in/sminnovations/smiadmin/ui/attendance/AttendanceDetailFragment.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/in/sminnovations/smiadmin/ui/attendance/EmployeeAttendanceListFragment.kt b/app/src/main/java/in/sminnovations/smiadmin/ui/attendance/EmployeeAttendanceListFragment.kt new file mode 100644 index 0000000..8c1c9cc --- /dev/null +++ b/app/src/main/java/in/sminnovations/smiadmin/ui/attendance/EmployeeAttendanceListFragment.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/in/sminnovations/smiadmin/ui/attendance/EmployeeListAdapter.kt b/app/src/main/java/in/sminnovations/smiadmin/ui/attendance/EmployeeListAdapter.kt new file mode 100644 index 0000000..34dbd4f --- /dev/null +++ b/app/src/main/java/in/sminnovations/smiadmin/ui/attendance/EmployeeListAdapter.kt @@ -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() { + + private val employees = mutableListOf() + + 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) { + employees.clear() + employees.addAll(newEmployees) + notifyDataSetChanged() + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_home.xml b/app/src/main/res/drawable/ic_home.xml new file mode 100644 index 0000000..20cb4d6 --- /dev/null +++ b/app/src/main/res/drawable/ic_home.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml new file mode 100644 index 0000000..6593f3a --- /dev/null +++ b/app/src/main/res/drawable/ic_settings.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 new file mode 100644 index 0000000..9e081b7 --- /dev/null +++ b/app/src/main/res/layout/fragment_attendance_detail.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_employee_attendance_list.xml b/app/src/main/res/layout/fragment_employee_attendance_list.xml new file mode 100644 index 0000000..7c11275 --- /dev/null +++ b/app/src/main/res/layout/fragment_employee_attendance_list.xml @@ -0,0 +1,23 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_employee_list.xml b/app/src/main/res/layout/fragment_employee_list.xml new file mode 100644 index 0000000..7c11275 --- /dev/null +++ b/app/src/main/res/layout/fragment_employee_list.xml @@ -0,0 +1,23 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml new file mode 100644 index 0000000..2ec505c --- /dev/null +++ b/app/src/main/res/layout/fragment_home.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml new file mode 100644 index 0000000..d3e63bb --- /dev/null +++ b/app/src/main/res/layout/fragment_settings.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_attendance.xml b/app/src/main/res/layout/item_attendance.xml new file mode 100644 index 0000000..626dbaa --- /dev/null +++ b/app/src/main/res/layout/item_attendance.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_employee.xml b/app/src/main/res/layout/item_employee.xml new file mode 100644 index 0000000..6a56dbd --- /dev/null +++ b/app/src/main/res/layout/item_employee.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_employee_simple.xml b/app/src/main/res/layout/item_employee_simple.xml new file mode 100644 index 0000000..2e22472 --- /dev/null +++ b/app/src/main/res/layout/item_employee_simple.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/bottom_nav_menu.xml b/app/src/main/res/menu/bottom_nav_menu.xml new file mode 100644 index 0000000..fab3986 --- /dev/null +++ b/app/src/main/res/menu/bottom_nav_menu.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml new file mode 100644 index 0000000..97be88e --- /dev/null +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/provider_paths.xml b/app/src/main/res/xml/provider_paths.xml new file mode 100644 index 0000000..80368a3 --- /dev/null +++ b/app/src/main/res/xml/provider_paths.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file