6 Commits
dev ... v_1.4

Author SHA1 Message Date
chandrashekhar reddy
5d691e9b1b PDFGenerator updated the format 2025-02-05 14:32:03 +05:30
chandrashekhar reddy
43c3137041 changes according to smi + iisc, added login screen, added filer, improved the UI 2025-02-04 17:27:33 +05:30
chandrashekhar reddy
830d347606 added fragments to add employee, download report in pdf format and send over mail, sharable qr codes 2025-01-31 13:59:02 +05:30
chandrashekhar reddy
27dd707e21 Merge remote-tracking branch 'refs/remotes/origin/dev' 2025-01-20 12:38:22 +05:30
sathwikcs
3e15b0df6b Updated the pdf generator, and improved pdf design 2025-01-17 16:41:23 +05:30
sathwikcs
3bb2b58ce1 Updated the pdf generator, and improved pdf design 2025-01-17 16:35:52 +05:30
34 changed files with 1755 additions and 176 deletions

1
.idea/misc.xml generated
View File

@@ -1,4 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" /> <component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">

55
.idea/other.xml generated
View File

@@ -124,6 +124,17 @@
<option name="screenX" value="2220" /> <option name="screenX" value="2220" />
<option name="screenY" value="1080" /> <option name="screenY" value="1080" />
</PersistentDeviceSelectionData> </PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="dm2q" />
<option name="id" value="dm2q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="S23 Plus" />
<option name="screenDensity" value="450" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData> <PersistentDeviceSelectionData>
<option name="api" value="34" /> <option name="api" value="34" />
<option name="brand" value="samsung" /> <option name="brand" value="samsung" />
@@ -146,6 +157,28 @@
<option name="screenX" value="1080" /> <option name="screenX" value="1080" />
<option name="screenY" value="2340" /> <option name="screenY" value="2340" />
</PersistentDeviceSelectionData> </PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="e3q" />
<option name="id" value="e3q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S24 Ultra" />
<option name="screenDensity" value="450" />
<option name="screenX" value="1440" />
<option name="screenY" value="3120" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="eos" />
<option name="id" value="eos" />
<option name="manufacturer" value="Google" />
<option name="name" value="Eos" />
<option name="screenDensity" value="320" />
<option name="screenX" value="384" />
<option name="screenY" value="384" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData> <PersistentDeviceSelectionData>
<option name="api" value="33" /> <option name="api" value="33" />
<option name="brand" value="google" /> <option name="brand" value="google" />
@@ -179,6 +212,17 @@
<option name="screenX" value="2208" /> <option name="screenX" value="2208" />
<option name="screenY" value="1840" /> <option name="screenY" value="1840" />
</PersistentDeviceSelectionData> </PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="motorola" />
<option name="codename" value="fogona" />
<option name="id" value="fogona" />
<option name="manufacturer" value="Motorola" />
<option name="name" value="moto g play - 2024" />
<option name="screenDensity" value="280" />
<option name="screenX" value="720" />
<option name="screenY" value="1600" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData> <PersistentDeviceSelectionData>
<option name="api" value="33" /> <option name="api" value="33" />
<option name="brand" value="samsung" /> <option name="brand" value="samsung" />
@@ -234,6 +278,17 @@
<option name="screenX" value="1080" /> <option name="screenX" value="1080" />
<option name="screenY" value="2400" /> <option name="screenY" value="2400" />
</PersistentDeviceSelectionData> </PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="motorola" />
<option name="codename" value="maui" />
<option name="id" value="maui" />
<option name="manufacturer" value="Motorola" />
<option name="name" value="moto g play - 2023" />
<option name="screenDensity" value="280" />
<option name="screenX" value="720" />
<option name="screenY" value="1600" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData> <PersistentDeviceSelectionData>
<option name="api" value="31" /> <option name="api" value="31" />
<option name="brand" value="google" /> <option name="brand" value="google" />

View File

@@ -13,8 +13,8 @@ android {
applicationId "in.sminnovations.smiadmin" applicationId "in.sminnovations.smiadmin"
minSdk 26 minSdk 26
targetSdk 34 targetSdk 34
versionCode 2 versionCode 4
versionName "1.1" versionName "1.4"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }
@@ -55,5 +55,6 @@ dependencies {
implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.cardview:cardview:1.0.0'
implementation 'com.google.android.material:material:1.11.0' implementation 'com.google.android.material:material:1.11.0'
implementation 'androidx.recyclerview:recyclerview:1.3.2' implementation 'androidx.recyclerview:recyclerview:1.3.2'
implementation 'com.google.zxing:core:3.4.1'
} }

View File

@@ -17,9 +17,9 @@
android:requestLegacyExternalStorage="true" android:requestLegacyExternalStorage="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher" android:icon="@drawable/smi_logo"
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@drawable/smi_logo"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.SMIAdmin" android:theme="@style/Theme.SMIAdmin"
tools:targetApi="31"> tools:targetApi="31">

View File

@@ -1,6 +1,7 @@
package `in`.sminnovations.smiadmin package `in`.sminnovations.smiadmin
import android.os.Bundle import android.os.Bundle
import android.view.View
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
@@ -29,7 +30,21 @@ class MainActivity : AppCompatActivity() {
navController = navHostFragment.navController navController = navHostFragment.navController
// Setup the bottom navigation with the NavController // Setup the bottom navigation with the NavController
binding.navView.setupWithNavController(navController) binding.navView?.let { bottomNav ->
bottomNav.setupWithNavController(navController)
// Hide bottom nav on specific destinations
navController.addOnDestinationChangedListener { _, destination, _ ->
when (destination.id) {
R.id.navigation_login -> {
bottomNav.visibility = View.GONE
}
else -> {
bottomNav.visibility = View.VISIBLE
}
}
}
}
// Set up navigation item selection listener // Set up navigation item selection listener
binding.navView.setOnItemSelectedListener { menuItem -> binding.navView.setOnItemSelectedListener { menuItem ->

View File

@@ -1,7 +1,15 @@
package `in`.sminnovations.smiadmin.data package `in`.sminnovations.smiadmin.data
data class Employee( data class Employee(
val Id: String = "", var Id: String = "",
val Name: String = "", var Name: String = "",
var EmployeeStatus: Boolean = false var EmployeeStatus: Boolean = false,
var Designation:String = "",
var DateOfJoining: String = "",
var dateOfBirth: String = "",
var Mobile: String = "",
var Email: String = "",
var CreatedAt: String = "",
var from: String="",
var role:String= "user"
) )

View File

@@ -10,7 +10,7 @@ import `in`.sminnovations.smiadmin.R
import `in`.sminnovations.smiadmin.data.Employee import `in`.sminnovations.smiadmin.data.Employee
class EmployeeListAdapter( class EmployeeListAdapter(
private val employees: MutableList<Employee> = mutableListOf(), val employees: MutableList<Employee> = mutableListOf(),
private val onStatusChanged: (Employee, Boolean) -> Unit private val onStatusChanged: (Employee, Boolean) -> Unit
) : RecyclerView.Adapter<EmployeeListAdapter.EmployeeViewHolder>() { ) : RecyclerView.Adapter<EmployeeListAdapter.EmployeeViewHolder>() {
@@ -32,7 +32,7 @@ class EmployeeListAdapter(
holder.statusSwitch.setOnCheckedChangeListener(null) holder.statusSwitch.setOnCheckedChangeListener(null)
// Set the text and switch state // Set the text and switch state
holder.nameTextView.text = employee.Name holder.nameTextView.text = employee.Name + " (" + employee.from + ")"
holder.statusSwitch.isChecked = employee.EmployeeStatus holder.statusSwitch.isChecked = employee.EmployeeStatus
// Set up the new listener after setting the checked state // Set up the new listener after setting the checked state

View File

@@ -0,0 +1,56 @@
package `in`.sminnovations.smiadmin.ui.adapter
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import `in`.sminnovations.smiadmin.data.Employee
import `in`.sminnovations.smiadmin.databinding.ItemEmployeeQrBinding
// EmployeeAdapter.kt (remains the same)
class EmployeeQrAdapter(
private val onShareClick: (Employee) -> Unit,
private val onItemClick: (Employee) -> Unit
) : ListAdapter<Employee, EmployeeQrAdapter.EmployeeViewHolder>(EmployeeDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EmployeeViewHolder {
val binding = ItemEmployeeQrBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return EmployeeViewHolder(binding)
}
override fun onBindViewHolder(holder: EmployeeViewHolder, position: Int) {
holder.bind(getItem(position))
}
inner class EmployeeViewHolder(
private val binding: ItemEmployeeQrBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(employee: Employee) {
binding.apply {
textEmployeeName.text = employee.Name + " (" + employee.from + ")"
textEmployeeId.text = employee.Id
buttonShare.setOnClickListener { onShareClick(employee) }
root.setOnClickListener { onItemClick(employee) }
}
}
}
}
// EmployeeDiffCallback.kt (remains the same)
class EmployeeDiffCallback : DiffUtil.ItemCallback<Employee>() {
override fun areItemsTheSame(oldItem: Employee, newItem: Employee): Boolean {
return oldItem.Id == newItem.Id
}
override fun areContentsTheSame(oldItem: Employee, newItem: Employee): Boolean {
return oldItem == newItem
}
}

View File

@@ -26,7 +26,7 @@ class EmployeeStatusListAdapter(
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val employee = employees[position] val employee = employees[position]
holder.nameTextView.text = employee.Name holder.nameTextView.text = employee.Name + " (" + employee.from + ")"
holder.itemView.setOnClickListener { holder.itemView.setOnClickListener {
onEmployeeClick(employee) onEmployeeClick(employee)
} }

View File

@@ -0,0 +1,176 @@
package `in`.sminnovations.smiadmin.ui.fragments
import android.app.DatePickerDialog
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.Toast
import androidx.navigation.fragment.findNavController
import com.google.firebase.firestore.FirebaseFirestore
import `in`.sminnovations.smiadmin.R
import `in`.sminnovations.smiadmin.data.Employee
import `in`.sminnovations.smiadmin.databinding.FragmentAddEmployeeBinding
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
import java.util.Locale
class AddEmployeeFragment : Fragment() {
private var _binding: FragmentAddEmployeeBinding? = null
private val binding get() = _binding!!
private val db = FirebaseFirestore.getInstance()
private var dateOfBirth: Calendar? = null
private var dateOfJoining: Calendar? = null
private val dateFormatter = SimpleDateFormat("dd/MM/yyyy", Locale.getDefault())
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentAddEmployeeBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val items = arrayOf("IISc", "SMI")
val adapter = ArrayAdapter(requireActivity(), R.layout.list_item, items)
binding.etEmployeeFrom.setAdapter(adapter)
binding.etEmployeeFrom.isFocusable = false
binding.etEmployeeFrom.isClickable = true
setupDatePickers()
setupSubmitButton()
}
private fun setupDatePickers() {
binding.etDateOfBirth.setOnClickListener {
showDatePicker(true)
}
binding.etDateOfJoining.setOnClickListener {
showDatePicker(false)
}
}
private fun showDatePicker(isDateOfBirth: Boolean) {
val calendar = Calendar.getInstance()
DatePickerDialog(
requireContext(),
{ _, year, month, day ->
calendar.set(year, month, day)
val formattedDate = dateFormatter.format(calendar.time)
if (isDateOfBirth) {
dateOfBirth = calendar.clone() as Calendar
binding.etDateOfBirth.setText(formattedDate)
} else {
dateOfJoining = calendar.clone() as Calendar
binding.etDateOfJoining.setText(formattedDate)
}
},
calendar.get(Calendar.YEAR),
calendar.get(Calendar.MONTH),
calendar.get(Calendar.DAY_OF_MONTH)
).show()
}
private fun generateEmployeeId(name: String, phone: String): String {
val namePrefix = name.take(4).uppercase()
val phoneSuffix = phone.takeLast(4)
return "$namePrefix$phoneSuffix"
}
private fun validateInputs(): Boolean {
var isValid = true
with(binding) {
if (etEmployeeName.text.isNullOrBlank()) {
etEmployeeName.error = "Name is required"
isValid = false
}
if (etEmployeeEmail.text.isNullOrBlank()) {
etEmployeeEmail.error = "Email is required"
isValid = false
} else if (!android.util.Patterns.EMAIL_ADDRESS.matcher(etEmployeeEmail.text.toString()).matches()) {
etEmployeeEmail.error = "Invalid email format"
isValid = false
}
if (etEmployeePhone.text.isNullOrBlank()) {
etEmployeePhone.error = "Phone number is required"
isValid = false
}
if (etEmployeePosition.text.isNullOrBlank()) {
etEmployeePosition.error = "Position is required"
isValid = false
}
if (dateOfBirth == null) {
etDateOfBirth.error = "Date of Birth is required"
isValid = false
}
if (dateOfJoining == null) {
etDateOfJoining.error = "Date of Joining is required"
isValid = false
}
}
return isValid
}
private fun saveEmployee() {
binding.btnSubmit.isEnabled = false // Prevent double submission
val name = binding.etEmployeeName.text.toString().trim()
val phone = binding.etEmployeePhone.text.toString().trim()
val employeeId = generateEmployeeId(name, phone)
val employee = Employee(
Id = employeeId,
Name = name,
Email = binding.etEmployeeEmail.text.toString().trim(),
Mobile = phone,
Designation = binding.etEmployeePosition.text.toString().trim(),
dateOfBirth = dateFormatter.format(dateOfBirth?.time ?: Date()),
DateOfJoining = dateFormatter.format(dateOfJoining?.time ?: Date()),
CreatedAt = dateFormatter.format(Date()), // Current date in dd/MM/yyyy format
from = binding.etEmployeeFrom.text.toString().trim(),
)
db.collection("employees")
.document(employeeId)
.set(employee)
.addOnSuccessListener {
Toast.makeText(context, "Employee added successfully", Toast.LENGTH_SHORT).show()
findNavController().navigateUp()
}
.addOnFailureListener { e ->
binding.btnSubmit.isEnabled = true
Toast.makeText(context, "Error: ${e.message}", Toast.LENGTH_SHORT).show()
}
}
private fun setupSubmitButton() {
binding.btnSubmit.setOnClickListener {
if (validateInputs()) {
saveEmployee()
}
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@@ -1,7 +1,9 @@
package `in`.sminnovations.smiadmin.ui.fragments package `in`.sminnovations.smiadmin.ui.fragments
import android.Manifest import android.Manifest
import android.app.AlertDialog
import android.app.ProgressDialog import android.app.ProgressDialog
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
@@ -12,6 +14,8 @@ import android.provider.Settings
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Button
import android.widget.RadioGroup
import android.widget.Toast import android.widget.Toast
import androidx.cardview.widget.CardView import androidx.cardview.widget.CardView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@@ -46,7 +50,12 @@ class HomeFragment : Fragment() {
view.findViewById<CardView>(R.id.cardAttendanceReport).setOnClickListener { view.findViewById<CardView>(R.id.cardAttendanceReport).setOnClickListener {
findNavController().navigate(R.id.action_home_to_employee_attendance_list) findNavController().navigate(R.id.action_home_to_employee_attendance_list)
} }
view.findViewById<CardView>(R.id.cardAddEmployee).setOnClickListener {
findNavController().navigate(R.id.action_home_to_add_employee)
}
view.findViewById<CardView>(R.id.cardEmployeeQr).setOnClickListener {
findNavController().navigate(R.id.action_to_employeeQrFragment)
}
view.findViewById<CardView>(R.id.cardDownloadReport).setOnClickListener { view.findViewById<CardView>(R.id.cardDownloadReport).setOnClickListener {
if (checkStoragePermission()) { if (checkStoragePermission()) {
showMonthYearPicker() showMonthYearPicker()
@@ -59,12 +68,39 @@ class HomeFragment : Fragment() {
private fun showMonthYearPicker() { private fun showMonthYearPicker() {
MonthYearPickerDialog().apply { MonthYearPickerDialog().apply {
setOnDateSelectedListener { year, month -> setOnDateSelectedListener { year, month ->
downloadAttendanceReport(year, month) showReportDialog(year, month, requireActivity())
//downloadAttendanceReport(year, month)
} }
}.show(parentFragmentManager, "MonthYearPicker") }.show(parentFragmentManager, "MonthYearPicker")
} }
private fun showReportDialog(year: Int, month: Int, context: Context) {
val dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_report_source, null)
val dialog = AlertDialog.Builder(context)
.setView(dialogView)
.setCancelable(true)
.create()
private fun downloadAttendanceReport(year: Int, month: Int) { val radioGroup = dialogView.findViewById<RadioGroup>(R.id.rgSource)
val downloadButton = dialogView.findViewById<Button>(R.id.btnDownload)
downloadButton.setOnClickListener {
val selectedSource = when (radioGroup.checkedRadioButtonId) {
R.id.rbSMI -> "SMI"
R.id.rbIISC -> "IISc"
else -> null
}
if (selectedSource != null) {
downloadAttendanceReport(year, month, selectedSource)
dialog.dismiss()
} else {
Toast.makeText(context, "Please select a source", Toast.LENGTH_SHORT).show()
}
}
dialog.show()
}
private fun downloadAttendanceReport(year: Int, month: Int, source: String) {
currentExcelTask?.cancel() currentExcelTask?.cancel()
val progressDialog = ProgressDialog(requireContext()).apply { val progressDialog = ProgressDialog(requireContext()).apply {
@@ -72,7 +108,7 @@ class HomeFragment : Fragment() {
setCancelable(false) setCancelable(false)
show() show()
} }
exportTask.exportAttendance(year, month) exportTask.exportAttendance(year, month,source)
progressDialog.dismiss() progressDialog.dismiss()
} }

View File

@@ -0,0 +1,175 @@
package `in`.sminnovations.smiadmin.ui.fragments
import android.content.Context
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.navigation.fragment.findNavController
import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.firebase.firestore.FirebaseFirestore
import `in`.sminnovations.smiadmin.MainActivity
import `in`.sminnovations.smiadmin.R
import `in`.sminnovations.smiadmin.databinding.FragmentLoginBinding
class LoginFragment : Fragment() {
private var _binding: FragmentLoginBinding? = null
private val binding get() = _binding!!
private lateinit var db: FirebaseFirestore
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentLoginBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
db = FirebaseFirestore.getInstance()
// Check if already logged in
val prefs = requireActivity().getSharedPreferences("admin_prefs", Context.MODE_PRIVATE)
val isLoggedIn = prefs.getBoolean("is_logged_in", false)
if (isLoggedIn) {
// Verify stored credentials in Firestore
binding.childLayout.visibility = View.GONE
binding.parentLayout.visibility = View.VISIBLE
val adminMobile = prefs.getString("admin_mobile", "") ?: ""
verifyAndNavigate(adminMobile)
} else {
binding.childLayout.visibility = View.VISIBLE
binding.parentLayout.visibility = View.GONE
}
binding.btnLogin.setOnClickListener {
val mobile = binding.etMobile.text.toString().trim()
val password = binding.etPassword.text.toString().trim()
when {
mobile.isEmpty() -> {
binding.etMobile.error = "Please enter mobile number"
return@setOnClickListener
}
mobile.length != 10 -> {
binding.etMobile.error = "Please enter valid mobile number"
return@setOnClickListener
}
password.isEmpty() -> {
binding.etPassword.error = "Please enter password"
return@setOnClickListener
}
else -> attemptLogin(mobile, password)
}
}
}
private fun verifyAndNavigate(mobile: String) {
if (mobile.isEmpty()) {
// If stored mobile is empty, clear prefs and show login
requireActivity().getSharedPreferences("admin_prefs", Context.MODE_PRIVATE)
.edit()
.clear()
.apply()
return
}
db.collection("employees")
.whereEqualTo("mobile", mobile)
.whereEqualTo("role", "admin")
.get()
.addOnSuccessListener { documents ->
if (!documents.isEmpty) {
// Still valid admin, navigate to home
navigateToHome()
} else {
// No longer an admin, clear prefs
requireActivity().getSharedPreferences("admin_prefs", Context.MODE_PRIVATE)
.edit()
.clear()
.apply()
}
}
.addOnFailureListener {
// On error, stay on login screen
showError("Error verifying credentials")
}
}
private fun attemptLogin(mobile: String, password: String) {
showLoading(true)
db.collection("employees")
.whereEqualTo("mobile", mobile)
.whereEqualTo("role", "admin")
.get()
.addOnSuccessListener { documents ->
if (documents.isEmpty) {
showLoading(false)
showError("Invalid credentials or not an admin user")
return@addOnSuccessListener
}
val document = documents.first()
val storedPassword = document.getString("password") ?: document.getString("Password")
if (storedPassword == password) {
// Save login state
val editor = requireActivity().getSharedPreferences(
"admin_prefs",
Context.MODE_PRIVATE
).edit()
editor.putBoolean("is_logged_in", true)
editor.putString("admin_id", document.id)
editor.putString("admin_mobile", mobile)
editor.apply()
showLoading(false)
navigateToHome()
} else {
showLoading(false)
showError("Invalid password")
}
}
.addOnFailureListener { e ->
showLoading(false)
showError("Error: ${e.message}")
}
}
private fun showLoading(show: Boolean) {
binding.progressBar.visibility = if (show) View.VISIBLE else View.GONE
binding.btnLogin.isEnabled = !show
}
private fun showError(message: String) {
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
}
private fun navigateToHome() {
findNavController().navigate(R.id.action_login_to_home)
}
override fun onResume() {
super.onResume()
// Hide bottom navigation
(activity as? MainActivity)?.findViewById<BottomNavigationView>(R.id.nav_view)?.visibility = View.GONE
}
override fun onStop() {
super.onStop()
// Show bottom navigation when leaving fragment
(activity as? MainActivity)?.findViewById<BottomNavigationView>(R.id.nav_view)?.visibility = View.VISIBLE
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@@ -1,5 +1,6 @@
package `in`.sminnovations.smiadmin.ui.fragments package `in`.sminnovations.smiadmin.ui.fragments
import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
@@ -7,6 +8,8 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import com.google.android.material.button.MaterialButton
import `in`.sminnovations.smiadmin.R import `in`.sminnovations.smiadmin.R
@@ -20,6 +23,10 @@ class SettingsFragment : Fragment() {
// Set app version // Set app version
val versionTextView: TextView = root.findViewById(R.id.textViewVersion) val versionTextView: TextView = root.findViewById(R.id.textViewVersion)
val button: MaterialButton = root.findViewById(R.id.logOutBtn)
button.setOnClickListener {
logout()
}
try { try {
val pInfo = requireContext().packageManager.getPackageInfo( val pInfo = requireContext().packageManager.getPackageInfo(
requireContext().packageName, 0 requireContext().packageName, 0
@@ -34,4 +41,12 @@ class SettingsFragment : Fragment() {
return root return root
} }
private fun logout() {
requireActivity().getSharedPreferences("admin_prefs", Context.MODE_PRIVATE)
.edit()
.clear()
.apply()
findNavController().navigate(R.id.action_settings_to_login)
}
} }

View File

@@ -0,0 +1,185 @@
package `in`.sminnovations.smiadmin.ui.fragments
import android.R
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Color
import android.net.Uri
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.ImageView
import android.widget.Toast
import androidx.core.content.FileProvider
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.firebase.firestore.FirebaseFirestore
import com.google.zxing.BarcodeFormat
import com.google.zxing.qrcode.QRCodeWriter
import `in`.sminnovations.smiadmin.data.Employee
import `in`.sminnovations.smiadmin.databinding.FragmentShareQrBinding
import `in`.sminnovations.smiadmin.ui.adapter.EmployeeQrAdapter
import java.io.File
import java.io.FileOutputStream
class ShareQrFragment : Fragment() {
private lateinit var binding: FragmentShareQrBinding
private lateinit var employeeAdapter: EmployeeQrAdapter
private val db = FirebaseFirestore.getInstance()
private var employeeList = emptyList<Employee>()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentShareQrBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupRecyclerView()
setupSpinner()
setupSearchView()
fetchEmployees()
}
private fun setupSearchView() {
binding.searchView.setOnQueryTextListener(object : androidx.appcompat.widget.SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
return false
}
override fun onQueryTextChange(newText: String?): Boolean {
filterEmployees(newText)
return true
}
})
}
private fun filterEmployees(query: String?) {
if (query.isNullOrBlank()) {
employeeAdapter.submitList(employeeList)
return
}
val filteredList = employeeList.filter {
it.Name.contains(query, ignoreCase = true)
}
employeeAdapter.submitList(filteredList)
}
private fun setupSpinner() {
val sortOptions = arrayOf("Name (A-Z)", "SMI First", "IISc First")
val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, sortOptions)
adapter.setDropDownViewResource(R.layout.simple_spinner_dropdown_item)
binding.spinnerSort.adapter = adapter
binding.spinnerSort.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
sortEmployees()
}
override fun onNothingSelected(parent: AdapterView<*>?) {}
}
}
private fun sortEmployees() {
val currentList = employeeAdapter.currentList.toMutableList()
when (binding.spinnerSort.selectedItemPosition) {
0 -> currentList.sortBy { it.Name?.lowercase() ?: "" }
1 -> currentList.sortByDescending { it.from == "SMI" } // Changed to sortByDescending
2 -> currentList.sortByDescending { it.from == "IISc" } // Changed to sortByDescending
}
employeeAdapter.submitList(currentList)
}
private fun setupRecyclerView() {
employeeAdapter = EmployeeQrAdapter(
onShareClick = { employee -> shareQRCode(employee) },
onItemClick = { employee -> showQRCode(employee) }
)
binding.recyclerViewEmployees.apply {
adapter = employeeAdapter
layoutManager = LinearLayoutManager(context)
}
}
private fun fetchEmployees() {
db.collection("employees")
.addSnapshotListener { snapshot, e ->
if (e != null) {
// Handle error
Toast.makeText(context, "Error fetching employees: ${e.message}", Toast.LENGTH_SHORT).show()
return@addSnapshotListener
}
employeeList = snapshot?.documents?.mapNotNull { doc ->
val employee = doc.toObject(Employee::class.java)
employee?.apply {
Id = doc.id
Name = doc.getString("name") ?: doc.getString("Name") ?: this.Name
from = doc.getString("from") ?: ""
}
} ?: emptyList()
// Sorting manually to handle both "name" and "Name"
val sortedList = employeeList.sortedBy { it.Name?.lowercase() }
employeeAdapter.submitList(sortedList)
}
}
private fun generateQRCode(employeeId: String): Bitmap {
val qrCodeWriter = QRCodeWriter()
val bitMatrix = qrCodeWriter.encode(employeeId, BarcodeFormat.QR_CODE, 512, 512)
val width = bitMatrix.width
val height = bitMatrix.height
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565)
for (x in 0 until width) {
for (y in 0 until height) {
bitmap.setPixel(x, y, if (bitMatrix[x, y]) Color.BLACK else Color.WHITE)
}
}
return bitmap
}
private fun showQRCode(employee: Employee) {
val qrBitmap = generateQRCode(employee.Id)
val dialog = MaterialAlertDialogBuilder(requireContext())
.setTitle("${employee.Name}'s QR Code")
.setView(ImageView(context).apply {
setImageBitmap(qrBitmap)
layoutParams = ViewGroup.LayoutParams(512, 512)
})
.setPositiveButton("Close") { dialog, _ -> dialog.dismiss() }
.create()
dialog.show()
}
private fun shareQRCode(employee: Employee) {
val qrBitmap = generateQRCode(employee.Id)
val shareIntent = Intent(Intent.ACTION_SEND).apply {
type = "image/*"
putExtra(Intent.EXTRA_STREAM, saveQRCodeToCache(qrBitmap, employee.Id))
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
startActivity(Intent.createChooser(shareIntent, "Share QR Code"))
}
private fun saveQRCodeToCache(bitmap: Bitmap, employeeId: String): Uri {
val file = File(requireContext().cacheDir, "qr_${employeeId}.png")
FileOutputStream(file).use { stream ->
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
}
return FileProvider.getUriForFile(
requireContext(),
"${requireContext().packageName}.provider",
file
)
}
}

View File

@@ -24,6 +24,9 @@ class AttendanceDetailFragment : Fragment() {
private val months = arrayOf("January", "February", "March", "April", "May", "June", private val months = arrayOf("January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December") "July", "August", "September", "October", "November", "December")
// Add years (you might want to adjust the range based on your needs)
private val years = (2024..2030).toList()
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@@ -41,11 +44,13 @@ class AttendanceDetailFragment : Fragment() {
binding.textViewEmployeeName.text = employeeName binding.textViewEmployeeName.text = employeeName
setupMonthSpinner() setupMonthSpinner()
setupYearSpinner()
setupRecyclerView() setupRecyclerView()
// Get current month (1-12) // Get current month (1-12) and year
val currentMonth = Calendar.getInstance().get(Calendar.MONTH) + 1 val currentMonth = Calendar.getInstance().get(Calendar.MONTH) + 1
fetchAttendanceData(employeeId, currentMonth) val currentYear = Calendar.getInstance().get(Calendar.YEAR)
fetchAttendanceData(employeeId, currentMonth, currentYear)
} }
private fun setupMonthSpinner() { private fun setupMonthSpinner() {
@@ -63,8 +68,35 @@ class AttendanceDetailFragment : Fragment() {
binding.spinnerMonth.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { binding.spinnerMonth.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
val employeeId = arguments?.getString("employeeId") ?: return val employeeId = arguments?.getString("employeeId") ?: return
// position + 1 because months in Calendar are 0-based val selectedYear = years[binding.spinnerYear.selectedItemPosition]
fetchAttendanceData(employeeId, position + 1) fetchAttendanceData(employeeId, position + 1, selectedYear)
}
override fun onNothingSelected(parent: AdapterView<*>?) {}
}
}
private fun setupYearSpinner() {
val adapter = ArrayAdapter(
requireContext(),
R.layout.simple_spinner_item,
years
)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
binding.spinnerYear.adapter = adapter
// Set current year as default
val currentYear = Calendar.getInstance().get(Calendar.YEAR)
val currentYearPosition = years.indexOf(currentYear)
if (currentYearPosition != -1) {
binding.spinnerYear.setSelection(currentYearPosition)
}
binding.spinnerYear.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
val employeeId = arguments?.getString("employeeId") ?: return
val selectedMonth = binding.spinnerMonth.selectedItemPosition + 1
fetchAttendanceData(employeeId, selectedMonth, years[position])
} }
override fun onNothingSelected(parent: AdapterView<*>?) {} override fun onNothingSelected(parent: AdapterView<*>?) {}
@@ -76,7 +108,7 @@ class AttendanceDetailFragment : Fragment() {
binding.recyclerViewAttendance.adapter = adapter binding.recyclerViewAttendance.adapter = adapter
} }
private fun fetchAttendanceData(employeeId: String, month: Int) { private fun fetchAttendanceData(employeeId: String, month: Int, year: Int) {
binding.progressBar.visibility = View.VISIBLE binding.progressBar.visibility = View.VISIBLE
db.collection("attendance_$employeeId") db.collection("attendance_$employeeId")
@@ -87,9 +119,10 @@ class AttendanceDetailFragment : Fragment() {
try { try {
val dateParts = doc.id.split("-") val dateParts = doc.id.split("-")
val docMonth = dateParts[1].toInt() val docMonth = dateParts[1].toInt()
val docYear = dateParts[2].toInt()
// Only include records for selected month // Only include records for selected month and year
if (docMonth == month) { if (docMonth == month && docYear == year) {
AttendanceData( AttendanceData(
date = doc.id, date = doc.id,
checkIn = doc.getString("checkIn") ?: "N/A", checkIn = doc.getString("checkIn") ?: "N/A",

View File

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

View File

@@ -1,9 +1,12 @@
package `in`.sminnovations.smiadmin.ui.fragments.attendance package `in`.sminnovations.smiadmin.ui.fragments.attendance
import android.R
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Toast import android.widget.Toast
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.google.firebase.firestore.FirebaseFirestore import com.google.firebase.firestore.FirebaseFirestore
@@ -15,7 +18,7 @@ import `in`.sminnovations.smiadmin.databinding.FragmentEmployeeListBinding
class EmployeeListFragment : Fragment() { class EmployeeListFragment : Fragment() {
private var _binding: FragmentEmployeeListBinding? = null private var _binding: FragmentEmployeeListBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
private var employeeList = emptyList<Employee>()
private lateinit var adapter: EmployeeListAdapter private lateinit var adapter: EmployeeListAdapter
private val db = FirebaseFirestore.getInstance() private val db = FirebaseFirestore.getInstance()
@@ -30,11 +33,58 @@ class EmployeeListFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
setupSpinner()
setupRecyclerView() setupRecyclerView()
setupSearchView()
fetchEmployees() fetchEmployees()
} }
private fun setupSearchView() {
binding.searchView.setOnQueryTextListener(object : androidx.appcompat.widget.SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
return false
}
override fun onQueryTextChange(newText: String?): Boolean {
filterEmployees(newText)
return true
}
})
}
private fun filterEmployees(query: String?) {
if (query.isNullOrBlank()) {
adapter.updateEmployees(employeeList)
return
}
val filteredList = employeeList.filter {
it.Name.contains(query, ignoreCase = true)
}
adapter.updateEmployees(filteredList)
}
private fun setupSpinner() {
val sortOptions = arrayOf("Name (A-Z)", "SMI First", "IISc First")
val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, sortOptions)
adapter.setDropDownViewResource(R.layout.simple_spinner_dropdown_item)
binding.spinnerSort.adapter = adapter
binding.spinnerSort.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
sortEmployees()
}
override fun onNothingSelected(parent: AdapterView<*>?) {}
}
}
private fun sortEmployees() {
val currentList = adapter.employees.toMutableList()
when (binding.spinnerSort.selectedItemPosition) {
0 -> currentList.sortBy { it.Name?.lowercase() ?: "" }
1 -> currentList.sortByDescending { it.from == "SMI" } // Changed to sortByDescending
2 -> currentList.sortByDescending { it.from == "IISc" } // Changed to sortByDescending
}
adapter.updateEmployees(currentList)
}
private fun setupRecyclerView() { private fun setupRecyclerView() {
adapter = EmployeeListAdapter { employee, newStatus -> adapter = EmployeeListAdapter { employee, newStatus ->
updateEmployeeStatus(employee, newStatus) updateEmployeeStatus(employee, newStatus)
@@ -56,13 +106,14 @@ class EmployeeListFragment : Fragment() {
} }
snapshot?.let { snapshot?.let {
val employeeList = snapshot.documents.map { doc -> employeeList = snapshot.documents.map { doc ->
Employee( Employee(
Id = doc.getString("Id") ?: "", Id = doc.getString("Id") ?: doc.getString("id") ?: "",
Name = doc.getString("Name") ?: "", Name = doc.getString("Name") ?: doc.getString("name") ?: "",
EmployeeStatus = doc.getBoolean("EmployeeStatus") ?: false EmployeeStatus = doc.getBoolean("EmployeeStatus") ?: false,
from = doc.getString("from") ?: ""
) )
} }.sortedBy { it.Name?.lowercase() }
adapter.updateEmployees(employeeList) adapter.updateEmployees(employeeList)
} }
} }

View File

@@ -1,10 +1,20 @@
package `in`.sminnovations.smiadmin.ui.utils package `in`.sminnovations.smiadmin.ui.utils
import android.Manifest
import android.app.ProgressDialog import android.app.ProgressDialog
import android.content.ActivityNotFoundException
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Environment
import android.widget.Toast import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.firebase.Firebase import com.google.firebase.Firebase
import com.google.firebase.firestore.Source
import com.google.firebase.firestore.firestore import com.google.firebase.firestore.firestore
import `in`.sminnovations.smiadmin.data.Constant import `in`.sminnovations.smiadmin.data.Constant
import `in`.sminnovations.smiadmin.data.Employee import `in`.sminnovations.smiadmin.data.Employee
@@ -13,16 +23,38 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.tasks.await import kotlinx.coroutines.tasks.await
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File
import java.io.IOException
class ExportTask(private val fragment: Fragment) { class ExportTask(private val fragment: Fragment) {
private val db = Firebase.firestore private val db = Firebase.firestore
private var currentExcelTask: Job? = null private var currentExcelTask: Job? = null
fun exportAttendance(year: Int, month: Int) { // Add permission check
private fun checkStoragePermission(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
true // Android 10 and above don't need explicit storage permission for Downloads
} else {
ContextCompat.checkSelfPermission(
fragment.requireContext(),
Manifest.permission.WRITE_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED
}
}
fun exportAttendance(year: Int, month: Int, source: String) {
if (!checkStoragePermission()) {
ActivityCompat.requestPermissions(
fragment.requireActivity(),
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
STORAGE_PERMISSION_CODE
)
return
}
// Cancel any existing task // Cancel any existing task
currentExcelTask?.cancel() currentExcelTask?.cancel()
// Show progress dialog
val progressDialog = ProgressDialog(fragment.requireContext()).apply { val progressDialog = ProgressDialog(fragment.requireContext()).apply {
setMessage("Generating attendance report...") setMessage("Generating attendance report...")
setCancelable(false) setCancelable(false)
@@ -31,34 +63,99 @@ class ExportTask(private val fragment: Fragment) {
currentExcelTask = fragment.lifecycleScope.launch(Dispatchers.Main) { currentExcelTask = fragment.lifecycleScope.launch(Dispatchers.Main) {
try { try {
val company = if (source == "SMI"){
Constant.COMPANY_NAME
}else{
"IISc-OMI_LAB"
}
// First get all employees // First get all employees
val employeesSnapshot = withContext(Dispatchers.IO) { val employeesSnapshot = withContext(Dispatchers.IO) {
db.collection("employees") db.collection("employees")
.whereEqualTo("EmployeeStatus", true) .whereEqualTo("EmployeeStatus", true)
.whereEqualTo("from",source)
.get() .get()
.await() .await()
} }
val employees = employeesSnapshot.documents.map { doc -> val employees = employeesSnapshot.documents.map { doc ->
Employee( Employee(
Id = doc.getString("Id") ?: "Unknown", Id = doc.getString("Id") ?: doc.getString("id") ?: "",
Name = doc.getString("Name") ?: "Unknown" Name = doc.getString("Name") ?: doc.getString("name") ?: "",
) )
}.sortedBy { it.Name } }.sortedBy { it.Name }
// Switch to IO dispatcher for file operations
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val pdfGenerator = PDFGenerator(fragment.requireContext(), Constant.COMPANY_NAME) val pdfGenerator = PDFGenerator(fragment.requireContext(), company)
pdfGenerator.generateAttendanceReport(employees, year, month) pdfGenerator.generateAttendanceReport(employees, year, month,source)
}.also { pdfFile -> }.also { pdfFile ->
// Switch back to Main dispatcher for UI updates withContext(Dispatchers.IO) {
withContext(Dispatchers.Main) { try {
progressDialog.dismiss() // Verify file exists and is readable
Toast.makeText( if (!pdfFile.exists() || !pdfFile.canRead()) {
fragment.context, throw IOException("Generated PDF file is not accessible")
"Report saved in Downloads folder: ${pdfFile.name}", }
Toast.LENGTH_LONG
).show() val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
val pdfFileInDownloads = File(downloadsDir, pdfFile.name)
// Copy file to Downloads if needed
if (!pdfFileInDownloads.exists()) {
pdfFile.copyTo(pdfFileInDownloads, overwrite = true)
}
// Verify the file was copied successfully
if (!pdfFileInDownloads.exists() || !pdfFileInDownloads.canRead()) {
throw IOException("Failed to copy PDF to Downloads folder")
}
val emailIntent = Intent(Intent.ACTION_SEND).apply {
type = "application/pdf"
putExtra(Intent.EXTRA_EMAIL, arrayOf("admin@sminnovations.in"))
putExtra(Intent.EXTRA_SUBJECT, "Attendance Report - $month $year")
putExtra(Intent.EXTRA_TEXT, "Please find attached the attendance report.")
try {
// Convert file URI to content URI for sharing
val contentUri = FileProvider.getUriForFile(
fragment.requireContext(),
"${fragment.requireContext().packageName}.provider",
pdfFileInDownloads
)
putExtra(Intent.EXTRA_STREAM, contentUri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
} catch (e: IllegalArgumentException) {
throw IOException("FileProvider not properly configured: ${e.message}")
}
}
// Switch to Main dispatcher for starting the email activity
withContext(Dispatchers.Main) {
progressDialog.dismiss()
try {
fragment.startActivity(Intent.createChooser(emailIntent, "Send Email"))
Toast.makeText(
fragment.context,
"Report saved in Downloads folder: ${pdfFile.name}",
Toast.LENGTH_LONG
).show()
} catch (e: ActivityNotFoundException) {
Toast.makeText(
fragment.context,
"No email app found",
Toast.LENGTH_SHORT
).show()
}
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
progressDialog.dismiss()
Toast.makeText(
fragment.context,
"Error sending email: ${e.message}",
Toast.LENGTH_SHORT
).show()
}
}
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
@@ -78,4 +175,8 @@ class ExportTask(private val fragment: Fragment) {
fun cleanup() { fun cleanup() {
currentExcelTask?.cancel() currentExcelTask?.cancel()
} }
companion object {
private const val STORAGE_PERMISSION_CODE = 101
}
} }

View File

@@ -1,19 +1,23 @@
package `in`.sminnovations.smiadmin.ui.utils package `in`.sminnovations.smiadmin.ui.utils
import android.content.Context import android.content.Context
import android.graphics.Color
import android.graphics.LinearGradient
import android.graphics.Paint
import android.graphics.Path
import android.graphics.Shader
import android.graphics.Typeface
import android.graphics.pdf.PdfDocument import android.graphics.pdf.PdfDocument
import android.os.Environment import android.os.Environment
import java.io.File
import java.io.FileOutputStream
import android.graphics.Paint
import android.graphics.Typeface
import android.graphics.Color
import com.google.firebase.Firebase import com.google.firebase.Firebase
import com.google.firebase.firestore.firestore import com.google.firebase.firestore.firestore
import `in`.sminnovations.smiadmin.data.Employee import `in`.sminnovations.smiadmin.data.Employee
import kotlinx.coroutines.tasks.await import kotlinx.coroutines.tasks.await
import java.io.File
import java.io.FileOutputStream
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.Calendar
import java.util.Locale
class PDFGenerator( class PDFGenerator(
private val context: Context, private val context: Context,
@@ -23,54 +27,69 @@ class PDFGenerator(
private lateinit var currentPage: PdfDocument.Page private lateinit var currentPage: PdfDocument.Page
private lateinit var canvas: android.graphics.Canvas private lateinit var canvas: android.graphics.Canvas
private var currentY: Float = 0f private var currentY: Float = 0f
private val startX = 40f
private val pageHeight = 842f // A4 height
private val pageWidth = 595f // A4 width
private var pageNumber = 1
// Define colors // Constants for page layout
private val primaryColor = Color.rgb(41, 128, 185) // Blue private val pageWidth = 595f // A4 width
private val secondaryColor = Color.rgb(44, 62, 80) // Dark Blue private val pageHeight = 842f // A4 height
private val borderColor = Color.rgb(189, 195, 199) // Light Gray private val margin = 40f
private val backgroundColor = Color.rgb(236, 240, 241) // Very Light Gray private val contentWidth = pageWidth - (2 * margin)
// Professional color scheme
private val primaryColor = Color.rgb(41, 128, 185) // Professional blue
private val secondaryColor = Color.rgb(44, 62, 80) // Dark blue
private val accentColor = Color.rgb(230, 126, 34) // Orange for highlights
private val backgroundColor = Color.rgb(236, 240, 241) // Light gray
private val borderColor = Color.rgb(189, 195, 199) // Medium gray
private val textColor = Color.rgb(52, 73, 94) // Dark gray for text
private var pageNumber = 1
suspend fun generateAttendanceReport( suspend fun generateAttendanceReport(
employees: List<Employee>, employees: List<Employee>,
selectedYear: Int, selectedYear: Int,
selectedMonth: Int selectedMonth: Int,
source: String
): File { ): File {
document = PdfDocument() document = PdfDocument()
createNewPage() createNewPage()
// Setup paints // Create paint objects
val headerPaint = Paint().apply { val titlePaint = Paint().apply {
textSize = 28f textSize = 32f
textAlign = Paint.Align.CENTER typeface = Typeface.create("sans-serif-light", Typeface.BOLD)
typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
color = primaryColor color = primaryColor
textAlign = Paint.Align.CENTER
} }
val subHeaderPaint = Paint().apply { val subtitlePaint = Paint().apply {
textSize = 20f textSize = 24f
textAlign = Paint.Align.CENTER typeface = Typeface.create("sans-serif-light", Typeface.NORMAL)
color = secondaryColor color = secondaryColor
textAlign = Paint.Align.CENTER
} }
val tableHeaderPaint = Paint().apply { val tableHeaderPaint = Paint().apply {
textSize = 14f textSize = 14f
textAlign = Paint.Align.LEFT typeface = Typeface.create("sans-serif-medium", Typeface.BOLD)
typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
color = Color.WHITE color = Color.WHITE
} }
val tablePaint = Paint().apply { val regularPaint = Paint().apply {
textSize = 12f textSize = 12f
textAlign = Paint.Align.LEFT typeface = Typeface.create("sans-serif", Typeface.NORMAL)
color = secondaryColor color = textColor
} }
// Draw headers and table val highlightPaint = Paint().apply {
drawPageHeaders(headerPaint, subHeaderPaint, selectedYear, selectedMonth) textSize = 12f
typeface = Typeface.create("sans-serif-medium", Typeface.BOLD)
color = accentColor
}
// Draw report header
drawReportHeader(selectedYear, selectedMonth, titlePaint, subtitlePaint)
// Draw table headers
drawTableHeaders(tableHeaderPaint) drawTableHeaders(tableHeaderPaint)
// Process each employee // Process each employee
@@ -78,7 +97,6 @@ class PDFGenerator(
if (currentY > pageHeight - 100) { if (currentY > pageHeight - 100) {
finishCurrentPage() finishCurrentPage()
createNewPage() createNewPage()
drawPageHeaders(headerPaint, subHeaderPaint, selectedYear, selectedMonth)
drawTableHeaders(tableHeaderPaint) drawTableHeaders(tableHeaderPaint)
} }
@@ -98,58 +116,210 @@ class PDFGenerator(
} }
.sorted() .sorted()
// Draw row background // Calculate working days (excluding holidays based on company)
val rowPaint = Paint().apply { val calendar = Calendar.getInstance().apply {
color = if ((employees.indexOf(employee) % 2) == 0) { set(selectedYear, selectedMonth - 1, 1)
backgroundColor }
} else { val totalDays = calendar.getActualMaximum(Calendar.DAY_OF_MONTH)
Color.WHITE
// Count holidays based on company name
val holidays = (1..totalDays).count { day ->
calendar.set(Calendar.DAY_OF_MONTH, day)
when (companyName) {
"ShanMukha Innovations" -> calendar.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY ||
calendar.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY
"IISc-OMI_LAB" -> calendar.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY
else -> calendar.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY // default case
} }
} }
val rowHeight = 60f val workingDays = totalDays - holidays
canvas.drawRect(startX, currentY, pageWidth - startX, currentY + rowHeight, rowPaint)
// Draw employee details // Calculate absent days (excluding holidays)
val nameY = currentY + 25f val absentDays = (1..totalDays).count { day ->
canvas.drawText(employee.Name, startX + 10f, nameY, tablePaint) calendar.set(Calendar.DAY_OF_MONTH, day)
canvas.drawText( val isHoliday = when (companyName) {
"${presentDates.size} days", "ShanMukha Innovations" -> calendar.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY ||
startX + 190f, calendar.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY
nameY, "IISc-OMI_LAB" -> calendar.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY
tablePaint else -> calendar.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY
) }
!isHoliday && !presentDates.contains(day)
// Format dates in groups of 5
val datesText = presentDates.chunked(5).joinToString("\n") { chunk ->
chunk.joinToString(", ") { day -> String.format("%02d", day) }
} }
// Draw dates in multiple lines if needed drawEmployeeRow(employee, presentDates, absentDays, workingDays, regularPaint, highlightPaint)
var dateY = nameY
datesText.split("\n").forEach { line ->
canvas.drawText(line, startX + 290f, dateY, tablePaint)
dateY += 15f
}
// Draw row borders
val borderPaint = Paint().apply {
style = Paint.Style.STROKE
strokeWidth = 0.5f
color = borderColor
}
canvas.drawLine(
startX, currentY + rowHeight,
pageWidth - startX, currentY + rowHeight,
borderPaint
)
currentY += rowHeight
} }
// Add footer with page numbers
drawFooter(regularPaint)
// Finish and save document // Finish and save document
finishCurrentPage() finishCurrentPage()
return saveDocument(selectedYear, selectedMonth)
}
private fun drawReportHeader(
selectedYear: Int,
selectedMonth: Int,
titlePaint: Paint,
subtitlePaint: Paint
) {
// Draw decorative header background
val headerPath = Path().apply {
moveTo(0f, 0f)
lineTo(pageWidth, 0f)
lineTo(pageWidth, 120f)
quadTo(pageWidth/2, 140f, 0f, 120f)
close()
}
canvas.drawPath(headerPath, Paint().apply {
color = primaryColor
alpha = 20
})
// Draw company name
canvas.drawText(companyName, pageWidth/2, 70f, titlePaint)
// Draw report title
val monthName = SimpleDateFormat("MMMM", Locale.getDefault())
.format(Calendar.getInstance().apply {
set(Calendar.MONTH, selectedMonth - 1)
}.time)
canvas.drawText(
"Monthly Attendance Summary",
pageWidth/2,
105f,
subtitlePaint
)
canvas.drawText(
"$monthName $selectedYear",
pageWidth/2,
135f,
subtitlePaint
)
currentY = 180f
}
private fun drawTableHeaders(headerPaint: Paint) {
val headerHeight = 45f
// Create gradient for header background
val gradient = LinearGradient(
margin, currentY,
margin, currentY + headerHeight,
primaryColor,
secondaryColor,
Shader.TileMode.CLAMP
)
// Draw header background with gradient
canvas.drawRoundRect(
margin, currentY,
pageWidth - margin,
currentY + headerHeight,
8f, 8f,
Paint().apply { shader = gradient }
)
// Draw column headers
var xPos = margin + 15f
canvas.drawText("Employee Name", xPos, currentY + 30f, headerPaint)
xPos += 180f
canvas.drawText("Days Present", xPos, currentY + 30f, headerPaint)
xPos += 100f
canvas.drawText("Days Absent", xPos, currentY + 30f, headerPaint)
xPos += 100f
canvas.drawText("Attendance Dates", xPos, currentY + 30f, headerPaint)
currentY += headerHeight + 5f
}
private fun drawEmployeeRow(
employee: Employee,
presentDates: List<Int>,
absentDays: Int,
workingDays: Int,
regularPaint: Paint,
highlightPaint: Paint
) {
val rowHeight = 60f
// Draw row background
canvas.drawRoundRect(
margin, currentY,
pageWidth - margin,
currentY + rowHeight,
4f, 4f,
Paint().apply {
color = backgroundColor
}
)
// Draw employee name
canvas.drawText(
employee.Name,
margin + 15f,
currentY + 10f,
regularPaint
)
// Draw days present
canvas.drawText(
"${presentDates.size} days",
margin + 195f,
currentY + 10f,
highlightPaint
)
// Draw days absent
canvas.drawText(
"$absentDays days",
margin + 295f,
currentY + 10f,
highlightPaint
)
// Draw attendance dates
val datesText = presentDates.chunked(5)
.joinToString("\n") { chunk ->
chunk.joinToString(", ") {
String.format("%02d", it)
}
}
var dateY = currentY + 10f
datesText.split("\n").forEach { line ->
canvas.drawText(
line,
margin + 395f,
dateY,
regularPaint
)
dateY += 10f
}
currentY += rowHeight + 10f
}
private fun drawFooter(paint: Paint) {
paint.textAlign = Paint.Align.CENTER
paint.textSize = 10f
canvas.drawText(
"Page $pageNumber",
pageWidth/2,
pageHeight - margin/2,
paint
)
}
private fun saveDocument(selectedYear: Int, selectedMonth: Int): File {
val fileName = "Attendance_Summary_${selectedYear}_${selectedMonth}.pdf" val fileName = "Attendance_Summary_${selectedYear}_${selectedMonth}.pdf"
val file = File( val file = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
@@ -165,58 +335,18 @@ class PDFGenerator(
} }
private fun createNewPage() { private fun createNewPage() {
val pageInfo = PdfDocument.PageInfo.Builder(pageWidth.toInt(), pageHeight.toInt(), pageNumber++).create() val pageInfo = PdfDocument.PageInfo.Builder(
pageWidth.toInt(),
pageHeight.toInt(),
pageNumber++
).create()
currentPage = document.startPage(pageInfo) currentPage = document.startPage(pageInfo)
canvas = currentPage.canvas canvas = currentPage.canvas
currentY = 120f currentY = margin
} }
private fun finishCurrentPage() { private fun finishCurrentPage() {
document.finishPage(currentPage) document.finishPage(currentPage)
} }
private fun drawPageHeaders(
headerPaint: Paint,
subHeaderPaint: Paint,
selectedYear: Int,
selectedMonth: Int
) {
// Draw company logo background
val logoBgPaint = Paint().apply {
color = primaryColor
alpha = 30
}
canvas.drawCircle(pageWidth / 2f, 45f, 100f, logoBgPaint)
// Draw company name
canvas.drawText(companyName, pageWidth / 2f, 60f, headerPaint)
// Draw report title
val monthName = SimpleDateFormat("MMMM", Locale.getDefault())
.format(Calendar.getInstance().apply {
set(Calendar.MONTH, selectedMonth - 1)
}.time)
val reportTitle = "Monthly Attendance Summary - $monthName $selectedYear"
canvas.drawText(reportTitle, pageWidth / 2f, 95f, subHeaderPaint)
currentY = 140f
}
private fun drawTableHeaders(headerPaint: Paint) {
// Draw header background
val headerBgPaint = Paint().apply {
color = primaryColor
}
val headerHeight = 40f
canvas.drawRect(startX, currentY, pageWidth - startX, currentY + headerHeight, headerBgPaint)
// Draw column headers
val textY = currentY + 25f
canvas.drawText("Employee Name", startX + 10f, textY, headerPaint)
canvas.drawText("Present Days", startX + 190f, textY, headerPaint)
canvas.drawText("Dates Present", startX + 290f, textY, headerPaint)
currentY += headerHeight
}
} }

View File

@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Select Report Source"
android:textSize="18sp"
android:textStyle="bold"
android:paddingBottom="8dp" />
<RadioGroup
android:id="@+id/rgSource"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<RadioButton
android:id="@+id/rbSMI"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="SMI" />
<RadioButton
android:id="@+id/rbIISC"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="IISC" />
</RadioGroup>
<Button
android:id="@+id/btnDownload"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Download" />
</LinearLayout>

View File

@@ -0,0 +1,122 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:orientation="vertical"
android:padding="16dp">
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etEmployeeName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Employee Name" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etEmployeeEmail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Email"
android:inputType="textEmailAddress" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etEmployeePhone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Phone Number"
android:maxLength="10"
android:inputType="phone" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etEmployeePosition"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Designation" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu">
<AutoCompleteTextView
android:id="@+id/etEmployeeFrom"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none"
android:hint="Working From" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etDateOfBirth"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Date of Birth"
android:focusable="false"
android:clickable="true" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etDateOfJoining"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Date of Joining"
android:focusable="false"
android:clickable="true" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/btnSubmit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="Submit" />
</LinearLayout>
</ScrollView>

View File

@@ -19,10 +19,17 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/textViewEmployeeName" /> app:layout_constraintTop_toBottomOf="@id/textViewEmployeeName" />
<Spinner
android:id="@+id/spinnerYear"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
app:layout_constraintStart_toEndOf="@id/spinnerMonth"
app:layout_constraintTop_toTopOf="@id/spinnerMonth" />
<TextView <TextView
android:id="@+id/textViewCount" android:id="@+id/textViewCount"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View File

@@ -4,10 +4,22 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<androidx.appcompat.widget.SearchView
android:id="@+id/searchView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:layout_constraintTop_toTopOf="parent"
app:iconifiedByDefault="false"
app:queryHint="Search by name"/>
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerViewEmployees" android:id="@+id/recyclerViewEmployees"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="0dp"
android:layout_marginTop="15dp"
app:layout_constraintTop_toBottomOf="@id/searchView"
app:layout_constraintBottom_toBottomOf="parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" /> app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
<ProgressBar <ProgressBar

View File

@@ -3,11 +3,28 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<androidx.appcompat.widget.SearchView
android:id="@+id/searchView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:layout_constraintTop_toTopOf="parent"
app:iconifiedByDefault="false"
app:queryHint="Search by name"/>
<Spinner
android:id="@+id/spinnerSort"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:layout_constraintTop_toBottomOf="@+id/searchView" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:layout_marginTop="15dp"
android:id="@+id/recyclerViewEmployees" android:id="@+id/recyclerViewEmployees"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/spinnerSort"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" /> app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
<ProgressBar <ProgressBar

View File

@@ -76,6 +76,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:cardCornerRadius="8dp" app:cardCornerRadius="8dp"
android:layout_marginBottom="16dp"
app:cardElevation="4dp"> app:cardElevation="4dp">
<LinearLayout <LinearLayout
@@ -98,6 +99,60 @@
android:text="Download attendance reports" /> android:text="Download attendance reports" />
</LinearLayout> </LinearLayout>
</androidx.cardview.widget.CardView> </androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:id="@+id/cardAddEmployee"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="8dp"
android:layout_marginBottom="16dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Add Employee"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Add employee to the system" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:id="@+id/cardEmployeeQr"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="8dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Share QR Code"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Share unique Qr code with Employees" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>

View File

@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:layout_gravity="center"
>
<LinearLayout
android:id="@+id/parentLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/smi_logo"
android:layout_margin="15dp"
android:orientation="vertical"
android:padding="16dp"
android:visibility="visible"
android:gravity="center">
</LinearLayout>
<LinearLayout
android:id="@+id/childLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp"
android:visibility="gone"
android:gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Admin Login"
android:textSize="24sp"
android:textStyle="bold"
android:layout_marginBottom="32dp"/>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etMobile"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Mobile Number"
android:inputType="phone"
android:maxLength="10"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
app:passwordToggleEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Password"
android:inputType="textPassword"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnLogin"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Login"
android:padding="12dp"/>
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:visibility="gone"/>
</LinearLayout>
</LinearLayout>

View File

@@ -47,4 +47,11 @@
android:text="SMI Admin App(Employee Attendance)" android:text="SMI Admin App(Employee Attendance)"
android:layout_marginTop="4dp" /> android:layout_marginTop="4dp" />
<com.google.android.material.button.MaterialButton
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Logout"
android:layout_marginTop="30dp"
android:id="@+id/logOutBtn"/>
</LinearLayout> </LinearLayout>

View File

@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.SearchView
android:id="@+id/searchView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:layout_constraintTop_toTopOf="parent"
app:iconifiedByDefault="false"
app:queryHint="Search by name"/>
<Spinner
android:id="@+id/spinnerSort"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:layout_constraintTop_toBottomOf="@+id/searchView" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerViewEmployees"
android:layout_width="match_parent"
android:layout_height="0dp"
android:clipToPadding="false"
android:padding="8dp"
android:layout_marginTop="15dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/spinnerSort"
tools:listitem="@layout/item_employee_qr" />
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<TextView
android:id="@+id/textEmpty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="No employees found"
android:textSize="16sp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:cardElevation="4dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<TextView
android:id="@+id/textEmployeeName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintEnd_toStartOf="@id/buttonShare"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/textEmployeeId"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
app:layout_constraintEnd_toStartOf="@id/buttonShare"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/textEmployeeName" />
<ImageButton
android:id="@+id/buttonShare"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="8dp"
android:src="@drawable/baseline_share_24"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="?attr/textAppearanceSubtitle1" />

View File

@@ -1,8 +1,20 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- mobile_navigation.xml -->
<navigation xmlns:android="http://schemas.android.com/apk/res/android" <navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/mobile_navigation" android:id="@+id/mobile_navigation"
app:startDestination="@+id/navigation_home"> app:startDestination="@+id/navigation_login">
<fragment
android:id="@+id/navigation_login"
android:name="in.sminnovations.smiadmin.ui.fragments.LoginFragment"
android:label="Login">
<action
android:id="@+id/action_login_to_home"
app:destination="@id/navigation_home"
app:popUpTo="@id/navigation_login"
app:popUpToInclusive="true" />
</fragment>
<fragment <fragment
android:id="@+id/navigation_home" android:id="@+id/navigation_home"
@@ -14,12 +26,24 @@
<action <action
android:id="@+id/action_home_to_employee_attendance_list" android:id="@+id/action_home_to_employee_attendance_list"
app:destination="@id/navigation_employee_attendance_list" /> app:destination="@id/navigation_employee_attendance_list" />
<action
android:id="@+id/action_home_to_add_employee"
app:destination="@id/navigation_add_employee" />
<action
android:id="@+id/action_to_employeeQrFragment"
app:destination="@id/navigation_employee_qr" />
</fragment> </fragment>
<fragment <fragment
android:id="@+id/navigation_settings" android:id="@+id/navigation_settings"
android:name="in.sminnovations.smiadmin.ui.fragments.SettingsFragment" android:name="in.sminnovations.smiadmin.ui.fragments.SettingsFragment"
android:label="Settings" /> android:label="Settings">
<action
android:id="@+id/action_settings_to_login"
app:destination="@id/navigation_login"
app:popUpTo="@id/mobile_navigation"
app:popUpToInclusive="true" />
</fragment>
<fragment <fragment
android:id="@+id/navigation_employee_list" android:id="@+id/navigation_employee_list"
@@ -35,6 +59,28 @@
app:destination="@id/navigation_attendance_detail" /> app:destination="@id/navigation_attendance_detail" />
</fragment> </fragment>
<fragment
android:id="@+id/navigation_add_employee"
android:name="in.sminnovations.smiadmin.ui.fragments.AddEmployeeFragment"
android:label="Add Employee">
<action
android:id="@+id/action_add_employee_to_home"
app:destination="@id/navigation_home"
app:popUpTo="@id/navigation_home"
app:popUpToInclusive="false" />
</fragment>
<fragment
android:id="@+id/navigation_employee_qr"
android:name="in.sminnovations.smiadmin.ui.fragments.ShareQrFragment"
android:label="Employee Qr">
<action
android:id="@+id/action_employee_qr_to_home"
app:destination="@id/navigation_home"
app:popUpTo="@id/navigation_home"
app:popUpToInclusive="false" />
</fragment>
<fragment <fragment
android:id="@+id/navigation_attendance_detail" android:id="@+id/navigation_attendance_detail"
android:name="in.sminnovations.smiadmin.ui.fragments.attendance.AttendanceDetailFragment" android:name="in.sminnovations.smiadmin.ui.fragments.attendance.AttendanceDetailFragment"

View File

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