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">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<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="screenY" value="1080" />
</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>
<option name="api" value="34" />
<option name="brand" value="samsung" />
@@ -146,6 +157,28 @@
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</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>
<option name="api" value="33" />
<option name="brand" value="google" />
@@ -179,6 +212,17 @@
<option name="screenX" value="2208" />
<option name="screenY" value="1840" />
</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>
<option name="api" value="33" />
<option name="brand" value="samsung" />
@@ -234,6 +278,17 @@
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</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>
<option name="api" value="31" />
<option name="brand" value="google" />

View File

@@ -13,8 +13,8 @@ android {
applicationId "in.sminnovations.smiadmin"
minSdk 26
targetSdk 34
versionCode 2
versionName "1.1"
versionCode 4
versionName "1.4"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@@ -55,5 +55,6 @@ dependencies {
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'com.google.android.material:material:1.11.0'
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:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:icon="@drawable/smi_logo"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:roundIcon="@drawable/smi_logo"
android:supportsRtl="true"
android:theme="@style/Theme.SMIAdmin"
tools:targetApi="31">

View File

@@ -1,6 +1,7 @@
package `in`.sminnovations.smiadmin
import android.os.Bundle
import android.view.View
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
@@ -29,7 +30,21 @@ class MainActivity : AppCompatActivity() {
navController = navHostFragment.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
binding.navView.setOnItemSelectedListener { menuItem ->

View File

@@ -1,7 +1,15 @@
package `in`.sminnovations.smiadmin.data
data class Employee(
val Id: String = "",
val Name: String = "",
var EmployeeStatus: Boolean = false
var Id: String = "",
var Name: String = "",
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
class EmployeeListAdapter(
private val employees: MutableList<Employee> = mutableListOf(),
val employees: MutableList<Employee> = mutableListOf(),
private val onStatusChanged: (Employee, Boolean) -> Unit
) : RecyclerView.Adapter<EmployeeListAdapter.EmployeeViewHolder>() {
@@ -32,7 +32,7 @@ class EmployeeListAdapter(
holder.statusSwitch.setOnCheckedChangeListener(null)
// Set the text and switch state
holder.nameTextView.text = employee.Name
holder.nameTextView.text = employee.Name + " (" + employee.from + ")"
holder.statusSwitch.isChecked = employee.EmployeeStatus
// 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) {
val employee = employees[position]
holder.nameTextView.text = employee.Name
holder.nameTextView.text = employee.Name + " (" + employee.from + ")"
holder.itemView.setOnClickListener {
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
import android.Manifest
import android.app.AlertDialog
import android.app.ProgressDialog
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
@@ -12,6 +14,8 @@ import android.provider.Settings
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.RadioGroup
import android.widget.Toast
import androidx.cardview.widget.CardView
import androidx.core.content.ContextCompat
@@ -46,7 +50,12 @@ class HomeFragment : Fragment() {
view.findViewById<CardView>(R.id.cardAttendanceReport).setOnClickListener {
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 {
if (checkStoragePermission()) {
showMonthYearPicker()
@@ -59,12 +68,39 @@ class HomeFragment : Fragment() {
private fun showMonthYearPicker() {
MonthYearPickerDialog().apply {
setOnDateSelectedListener { year, month ->
downloadAttendanceReport(year, month)
showReportDialog(year, month, requireActivity())
//downloadAttendanceReport(year, month)
}
}.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()
val progressDialog = ProgressDialog(requireContext()).apply {
@@ -72,7 +108,7 @@ class HomeFragment : Fragment() {
setCancelable(false)
show()
}
exportTask.exportAttendance(year, month)
exportTask.exportAttendance(year, month,source)
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
import android.content.Context
import android.content.pm.PackageManager
import android.os.Bundle
import android.view.LayoutInflater
@@ -7,6 +8,8 @@ import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import com.google.android.material.button.MaterialButton
import `in`.sminnovations.smiadmin.R
@@ -20,6 +23,10 @@ class SettingsFragment : Fragment() {
// Set app version
val versionTextView: TextView = root.findViewById(R.id.textViewVersion)
val button: MaterialButton = root.findViewById(R.id.logOutBtn)
button.setOnClickListener {
logout()
}
try {
val pInfo = requireContext().packageManager.getPackageInfo(
requireContext().packageName, 0
@@ -34,4 +41,12 @@ class SettingsFragment : Fragment() {
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",
"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(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -41,11 +44,13 @@ class AttendanceDetailFragment : Fragment() {
binding.textViewEmployeeName.text = employeeName
setupMonthSpinner()
setupYearSpinner()
setupRecyclerView()
// Get current month (1-12)
// Get current month (1-12) and year
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() {
@@ -63,8 +68,35 @@ class AttendanceDetailFragment : Fragment() {
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)
val selectedYear = years[binding.spinnerYear.selectedItemPosition]
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<*>?) {}
@@ -76,7 +108,7 @@ class AttendanceDetailFragment : Fragment() {
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
db.collection("attendance_$employeeId")
@@ -87,9 +119,10 @@ class AttendanceDetailFragment : Fragment() {
try {
val dateParts = doc.id.split("-")
val docMonth = dateParts[1].toInt()
val docYear = dateParts[2].toInt()
// Only include records for selected month
if (docMonth == month) {
// Only include records for selected month and year
if (docMonth == month && docYear == year) {
AttendanceData(
date = doc.id,
checkIn = doc.getString("checkIn") ?: "N/A",

View File

@@ -19,6 +19,7 @@ class EmployeeAttendanceListFragment : Fragment() {
private lateinit var adapter: EmployeeStatusListAdapter
private val db = FirebaseFirestore.getInstance()
private var allEmployees = listOf<Employee>() // Store all employees
override fun onCreateView(
inflater: LayoutInflater,
@@ -33,9 +34,35 @@ class EmployeeAttendanceListFragment : Fragment() {
super.onViewCreated(view, savedInstanceState)
setupRecyclerView()
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()) {
adapter.updateEmployees(allEmployees)
return
}
val filteredList = allEmployees.filter {
it.Name.contains(query, ignoreCase = true)
}
adapter.updateEmployees(filteredList)
}
private fun setupRecyclerView() {
adapter = EmployeeStatusListAdapter { employee ->
val bundle = Bundle().apply {
@@ -57,13 +84,19 @@ class EmployeeAttendanceListFragment : Fragment() {
.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"
)
allEmployees = snapshot.documents.mapNotNull { doc ->
val status = doc.getBoolean("EmployeeStatus") ?: true // Default to true if field is missing
if (status) {
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 }
adapter.updateEmployees(employees)
adapter.updateEmployees(allEmployees)
}
.addOnFailureListener { e ->
binding.progressBar.visibility = View.GONE

View File

@@ -1,9 +1,12 @@
package `in`.sminnovations.smiadmin.ui.fragments.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
@@ -15,7 +18,7 @@ import `in`.sminnovations.smiadmin.databinding.FragmentEmployeeListBinding
class EmployeeListFragment : Fragment() {
private var _binding: FragmentEmployeeListBinding? = null
private val binding get() = _binding!!
private var employeeList = emptyList<Employee>()
private lateinit var adapter: EmployeeListAdapter
private val db = FirebaseFirestore.getInstance()
@@ -30,11 +33,58 @@ class EmployeeListFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupSpinner()
setupRecyclerView()
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()) {
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() {
adapter = EmployeeListAdapter { employee, newStatus ->
updateEmployeeStatus(employee, newStatus)
@@ -56,13 +106,14 @@ class EmployeeListFragment : Fragment() {
}
snapshot?.let {
val employeeList = snapshot.documents.map { doc ->
employeeList = snapshot.documents.map { doc ->
Employee(
Id = doc.getString("Id") ?: "",
Name = doc.getString("Name") ?: "",
EmployeeStatus = doc.getBoolean("EmployeeStatus") ?: false
Id = doc.getString("Id") ?: doc.getString("id") ?: "",
Name = doc.getString("Name") ?: doc.getString("name") ?: "",
EmployeeStatus = doc.getBoolean("EmployeeStatus") ?: false,
from = doc.getString("from") ?: ""
)
}
}.sortedBy { it.Name?.lowercase() }
adapter.updateEmployees(employeeList)
}
}

View File

@@ -1,10 +1,20 @@
package `in`.sminnovations.smiadmin.ui.utils
import android.Manifest
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 androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import com.google.firebase.Firebase
import com.google.firebase.firestore.Source
import com.google.firebase.firestore.firestore
import `in`.sminnovations.smiadmin.data.Constant
import `in`.sminnovations.smiadmin.data.Employee
@@ -13,16 +23,38 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.tasks.await
import kotlinx.coroutines.withContext
import java.io.File
import java.io.IOException
class ExportTask(private val fragment: Fragment) {
private val db = Firebase.firestore
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
currentExcelTask?.cancel()
// Show progress dialog
val progressDialog = ProgressDialog(fragment.requireContext()).apply {
setMessage("Generating attendance report...")
setCancelable(false)
@@ -31,34 +63,99 @@ class ExportTask(private val fragment: Fragment) {
currentExcelTask = fragment.lifecycleScope.launch(Dispatchers.Main) {
try {
val company = if (source == "SMI"){
Constant.COMPANY_NAME
}else{
"IISc-OMI_LAB"
}
// First get all employees
val employeesSnapshot = withContext(Dispatchers.IO) {
db.collection("employees")
.whereEqualTo("EmployeeStatus", true)
.whereEqualTo("from",source)
.get()
.await()
}
val employees = employeesSnapshot.documents.map { doc ->
Employee(
Id = doc.getString("Id") ?: "Unknown",
Name = doc.getString("Name") ?: "Unknown"
Id = doc.getString("Id") ?: doc.getString("id") ?: "",
Name = doc.getString("Name") ?: doc.getString("name") ?: "",
)
}.sortedBy { it.Name }
// Switch to IO dispatcher for file operations
withContext(Dispatchers.IO) {
val pdfGenerator = PDFGenerator(fragment.requireContext(), Constant.COMPANY_NAME)
pdfGenerator.generateAttendanceReport(employees, year, month)
val pdfGenerator = PDFGenerator(fragment.requireContext(), company)
pdfGenerator.generateAttendanceReport(employees, year, month,source)
}.also { pdfFile ->
// Switch back to Main dispatcher for UI updates
withContext(Dispatchers.Main) {
progressDialog.dismiss()
Toast.makeText(
fragment.context,
"Report saved in Downloads folder: ${pdfFile.name}",
Toast.LENGTH_LONG
).show()
withContext(Dispatchers.IO) {
try {
// Verify file exists and is readable
if (!pdfFile.exists() || !pdfFile.canRead()) {
throw IOException("Generated PDF file is not accessible")
}
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) {
@@ -78,4 +175,8 @@ class ExportTask(private val fragment: Fragment) {
fun cleanup() {
currentExcelTask?.cancel()
}
companion object {
private const val STORAGE_PERMISSION_CODE = 101
}
}

View File

@@ -1,19 +1,23 @@
package `in`.sminnovations.smiadmin.ui.utils
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.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.firestore.firestore
import `in`.sminnovations.smiadmin.data.Employee
import kotlinx.coroutines.tasks.await
import java.io.File
import java.io.FileOutputStream
import java.text.SimpleDateFormat
import java.util.*
import java.util.Calendar
import java.util.Locale
class PDFGenerator(
private val context: Context,
@@ -23,54 +27,69 @@ class PDFGenerator(
private lateinit var currentPage: PdfDocument.Page
private lateinit var canvas: android.graphics.Canvas
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
private val primaryColor = Color.rgb(41, 128, 185) // Blue
private val secondaryColor = Color.rgb(44, 62, 80) // Dark Blue
private val borderColor = Color.rgb(189, 195, 199) // Light Gray
private val backgroundColor = Color.rgb(236, 240, 241) // Very Light Gray
// Constants for page layout
private val pageWidth = 595f // A4 width
private val pageHeight = 842f // A4 height
private val margin = 40f
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(
employees: List<Employee>,
selectedYear: Int,
selectedMonth: Int
selectedMonth: Int,
source: String
): File {
document = PdfDocument()
createNewPage()
// Setup paints
val headerPaint = Paint().apply {
textSize = 28f
textAlign = Paint.Align.CENTER
typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
// Create paint objects
val titlePaint = Paint().apply {
textSize = 32f
typeface = Typeface.create("sans-serif-light", Typeface.BOLD)
color = primaryColor
textAlign = Paint.Align.CENTER
}
val subHeaderPaint = Paint().apply {
textSize = 20f
textAlign = Paint.Align.CENTER
val subtitlePaint = Paint().apply {
textSize = 24f
typeface = Typeface.create("sans-serif-light", Typeface.NORMAL)
color = secondaryColor
textAlign = Paint.Align.CENTER
}
val tableHeaderPaint = Paint().apply {
textSize = 14f
textAlign = Paint.Align.LEFT
typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
typeface = Typeface.create("sans-serif-medium", Typeface.BOLD)
color = Color.WHITE
}
val tablePaint = Paint().apply {
val regularPaint = Paint().apply {
textSize = 12f
textAlign = Paint.Align.LEFT
color = secondaryColor
typeface = Typeface.create("sans-serif", Typeface.NORMAL)
color = textColor
}
// Draw headers and table
drawPageHeaders(headerPaint, subHeaderPaint, selectedYear, selectedMonth)
val highlightPaint = Paint().apply {
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)
// Process each employee
@@ -78,7 +97,6 @@ class PDFGenerator(
if (currentY > pageHeight - 100) {
finishCurrentPage()
createNewPage()
drawPageHeaders(headerPaint, subHeaderPaint, selectedYear, selectedMonth)
drawTableHeaders(tableHeaderPaint)
}
@@ -98,58 +116,210 @@ class PDFGenerator(
}
.sorted()
// Draw row background
val rowPaint = Paint().apply {
color = if ((employees.indexOf(employee) % 2) == 0) {
backgroundColor
} else {
Color.WHITE
// Calculate working days (excluding holidays based on company)
val calendar = Calendar.getInstance().apply {
set(selectedYear, selectedMonth - 1, 1)
}
val totalDays = calendar.getActualMaximum(Calendar.DAY_OF_MONTH)
// 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
canvas.drawRect(startX, currentY, pageWidth - startX, currentY + rowHeight, rowPaint)
val workingDays = totalDays - holidays
// Draw employee details
val nameY = currentY + 25f
canvas.drawText(employee.Name, startX + 10f, nameY, tablePaint)
canvas.drawText(
"${presentDates.size} days",
startX + 190f,
nameY,
tablePaint
)
// Format dates in groups of 5
val datesText = presentDates.chunked(5).joinToString("\n") { chunk ->
chunk.joinToString(", ") { day -> String.format("%02d", day) }
// Calculate absent days (excluding holidays)
val absentDays = (1..totalDays).count { day ->
calendar.set(Calendar.DAY_OF_MONTH, day)
val isHoliday = 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
}
!isHoliday && !presentDates.contains(day)
}
// Draw dates in multiple lines if needed
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
drawEmployeeRow(employee, presentDates, absentDays, workingDays, regularPaint, highlightPaint)
}
// Add footer with page numbers
drawFooter(regularPaint)
// Finish and save document
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 file = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
@@ -165,58 +335,18 @@ class PDFGenerator(
}
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)
canvas = currentPage.canvas
currentY = 120f
currentY = margin
}
private fun finishCurrentPage() {
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_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
app:layout_constraintStart_toStartOf="parent"
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
android:id="@+id/textViewCount"
android:layout_width="wrap_content"

View File

@@ -4,10 +4,22 @@
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"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerViewEmployees"
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" />
<ProgressBar

View File

@@ -3,11 +3,28 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
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:layout_marginTop="15dp"
android:id="@+id/recyclerViewEmployees"
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" />
<ProgressBar

View File

@@ -76,6 +76,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="8dp"
android:layout_marginBottom="16dp"
app:cardElevation="4dp">
<LinearLayout
@@ -98,6 +99,60 @@
android:text="Download attendance reports" />
</LinearLayout>
</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>
</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: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>

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"?>
<!-- mobile_navigation.xml -->
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/mobile_navigation"
app:startDestination="@+id/navigation_home">
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
android:id="@+id/navigation_home"
@@ -14,12 +26,24 @@
<action
android:id="@+id/action_home_to_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
android:id="@+id/navigation_settings"
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
android:id="@+id/navigation_employee_list"
@@ -35,6 +59,28 @@
app:destination="@id/navigation_attendance_detail" />
</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
android:id="@+id/navigation_attendance_detail"
android:name="in.sminnovations.smiadmin.ui.fragments.attendance.AttendanceDetailFragment"

View File

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