18 Commits
v_1.4 ... main

Author SHA1 Message Date
sathwikcs
6ed17b3e07 updated the PDFGenerator daily.kt 2025-04-08 12:28:38 +05:30
sathwikcs
c9db1cb727 updated the PDFGenerator daily.kt 2025-03-24 15:08:37 +05:30
sathwikcs
5489aeb588 updated the dialog_date_month_year_picker.xml 2025-03-21 18:03:34 +05:30
sathwikcs
1cebe79071 Fixed the logo in splash screen 2025-03-21 18:03:06 +05:30
sathwikcs
aed649bf7d updated DailyMonthYearPickerDialog 2025-03-21 18:02:34 +05:30
sathwikcs
e2e5733d4b updated build.gradle 2025-03-21 18:01:36 +05:30
sathwikcs
419e628d06 updated other.xml 2025-03-18 17:45:29 +05:30
sathwikcs
b269993e13 updated PDFGenerator daily.kt 2025-03-18 17:44:55 +05:30
sathwikcs
53149d0361 created PDFGenerator daily.kt 2025-03-18 17:44:14 +05:30
sathwikcs
811f71049d created dialog_date_month_year_picker.xml 2025-03-18 17:43:28 +05:30
sathwikcs
9c3156a85a created DailyMonthYearPickerDialog 2025-03-18 17:42:48 +05:30
sathwikcs
2867ecce4f created dailyReportExport.kt 2025-03-18 17:42:13 +05:30
sathwikcs
6c90a1a183 added the home fragment_home.xml 2025-03-18 17:41:46 +05:30
sathwikcs
43d6979fac Fixed the issue when adding employee 2025-03-03 13:09:46 +05:30
sathwikcs
43317574e9 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	.idea/other.xml
#	app/src/main/java/in/sminnovations/smiadmin/data/Employee.kt
2025-03-03 13:09:08 +05:30
sathwikcs
b7d0fd32a9 Fixed the issue when adding employee 2025-03-03 13:05:08 +05:30
chandrashekhar reddy
a72c062b96 expandable attendance list bug fix 2025-02-21 13:14:56 +05:30
chandrashekhar reddy
304f2cbe60 expandable attendance list and network check 2025-02-12 11:14:57 +05:30
21 changed files with 1509 additions and 118 deletions

154
.idea/other.xml generated
View File

@@ -14,6 +14,17 @@
<option name="screenX" value="720" />
<option name="screenY" value="1280" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="OnePlus" />
<option name="codename" value="OP5552L1" />
<option name="id" value="OP5552L1" />
<option name="manufacturer" value="OnePlus" />
<option name="name" value="CPH2415" />
<option name="screenDensity" value="480" />
<option name="screenX" value="1080" />
<option name="screenY" value="2412" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="OPPO" />
@@ -47,6 +58,28 @@
<option name="screenX" value="1840" />
<option name="screenY" value="2944" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="a15" />
<option name="id" value="a15" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="A15" />
<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" />
<option name="codename" value="a35x" />
<option name="id" value="a35x" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="A35" />
<option name="screenDensity" value="450" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="31" />
<option name="brand" value="samsung" />
@@ -69,6 +102,28 @@
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="motorola" />
<option name="codename" value="arcfox" />
<option name="id" value="arcfox" />
<option name="manufacturer" value="Motorola" />
<option name="name" value="razr plus 2024" />
<option name="screenDensity" value="360" />
<option name="screenX" value="1080" />
<option name="screenY" value="1272" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="motorola" />
<option name="codename" value="austin" />
<option name="id" value="austin" />
<option name="manufacturer" value="Motorola" />
<option name="name" value="moto g 5G (2022)" />
<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" />
@@ -223,6 +278,39 @@
<option name="screenX" value="720" />
<option name="screenY" value="1600" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="g0q" />
<option name="id" value="g0q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="SM-S906U1" />
<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" />
<option name="codename" value="gta9pwifi" />
<option name="id" value="gta9pwifi" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="SM-X210" />
<option name="screenDensity" value="240" />
<option name="screenX" value="1200" />
<option name="screenY" value="1920" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="gts7xllite" />
<option name="id" value="gts7xllite" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="SM-T738U" />
<option name="screenDensity" value="340" />
<option name="screenX" value="1600" />
<option name="screenY" value="2560" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="samsung" />
@@ -234,6 +322,28 @@
<option name="screenX" value="1848" />
<option name="screenY" value="2960" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="gts8wifi" />
<option name="id" value="gts8wifi" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Tab S8" />
<option name="screenDensity" value="274" />
<option name="screenX" value="1600" />
<option name="screenY" value="2560" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="gts9fe" />
<option name="id" value="gts9fe" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Tab S9 FE 5G" />
<option name="screenDensity" value="280" />
<option name="screenX" value="1440" />
<option name="screenY" value="2304" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
@@ -289,6 +399,17 @@
<option name="screenX" value="720" />
<option name="screenY" value="1600" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="o1q" />
<option name="id" value="o1q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S21" />
<option name="screenDensity" value="421" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="31" />
<option name="brand" value="google" />
@@ -345,6 +466,17 @@
<option name="screenY" value="384" />
<option name="type" value="WEAR_OS" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="r11q" />
<option name="id" value="r11q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="SM-S711U" />
<option name="screenDensity" value="450" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="30" />
<option name="brand" value="google" />
@@ -367,6 +499,17 @@
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="t2q" />
<option name="id" value="t2q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S21 Plus" />
<option name="screenDensity" value="394" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
@@ -389,6 +532,17 @@
<option name="screenX" value="1080" />
<option name="screenY" value="2424" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="35" />
<option name="brand" value="google" />
<option name="codename" value="tokay" />
<option name="id" value="tokay" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2424" />
</PersistentDeviceSelectionData>
</list>
</option>
</component>

View File

@@ -13,8 +13,8 @@ android {
applicationId "in.sminnovations.smiadmin"
minSdk 26
targetSdk 34
versionCode 4
versionName "1.4"
versionCode 7
versionName "1.8-beta"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

View File

@@ -0,0 +1,81 @@
package `in`.sminnovations.smiadmin
import android.content.Context
import android.content.Intent
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import `in`.sminnovations.smiadmin.ui.utils.NetworkUtils
abstract class BaseActivity : AppCompatActivity() {
private var internetDialog: AlertDialog? = null
private lateinit var networkUtils: NetworkUtils
private lateinit var connectivityCallback: ConnectivityManager.NetworkCallback
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
networkUtils = NetworkUtils(this)
setupNetworkCallback()
}
private fun setupNetworkCallback() {
val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
connectivityCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
runOnUiThread {
internetDialog?.dismiss()
}
}
override fun onLost(network: Network) {
runOnUiThread {
showNoInternetDialog()
}
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
connectivityManager.registerDefaultNetworkCallback(connectivityCallback)
} else {
val request = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.build()
connectivityManager.registerNetworkCallback(request, connectivityCallback)
}
}
private fun showNoInternetDialog() {
if (internetDialog?.isShowing == true) return
internetDialog = AlertDialog.Builder(this)
.setTitle("No Internet Connection")
.setMessage("Please turn on your mobile data or connect to Wi-Fi")
.setCancelable(false)
.setPositiveButton("Open Settings") { _, _ ->
startActivity(Intent(Settings.ACTION_WIRELESS_SETTINGS))
}
.create()
internetDialog?.show()
}
override fun onResume() {
super.onResume()
if (!networkUtils.isInternetAvailable()) {
showNoInternetDialog()
}
}
override fun onDestroy() {
super.onDestroy()
val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
connectivityManager.unregisterNetworkCallback(connectivityCallback)
internetDialog?.dismiss()
}
}

View File

@@ -13,7 +13,7 @@ import androidx.navigation.ui.setupWithNavController
import com.google.android.material.bottomnavigation.BottomNavigationView
import `in`.sminnovations.smiadmin.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
class MainActivity : BaseActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var navController: NavController

View File

@@ -2,6 +2,7 @@ package `in`.sminnovations.smiadmin.data
data class AttendanceData(
val date: String = "",
val checkIn: String = "",
val checkOut: String = ""
val checkIns: Map<String, String>,
val checkOuts: Map<String, String>,
var isExpanded: Boolean = false
)

View File

@@ -1,15 +1,41 @@
package `in`.sminnovations.smiadmin.data
import com.google.firebase.firestore.PropertyName
data class Employee(
@PropertyName("Id")
var Id: String = "",
@PropertyName("Name")
var Name: String = "",
var EmployeeStatus: Boolean = false,
var Designation:String = "",
@PropertyName("EmployeeStatus")
var EmployeeStatus: Boolean = true,
@PropertyName("Designation")
var Designation: String = "",
@PropertyName("DateOfJoining")
var DateOfJoining: String = "",
@PropertyName("dateOfBirth")
var dateOfBirth: String = "",
@PropertyName("Mobile")
var Mobile: String = "",
@PropertyName("Email")
var Email: String = "",
@PropertyName("CreatedAt")
var CreatedAt: String = "",
var from: String="",
var role:String= "user"
@PropertyName("from")
var from: String = "",
@PropertyName("role")
var role: String = "user"
)

View File

@@ -1,9 +1,13 @@
package `in`.sminnovations.smiadmin.ui.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import androidx.cardview.widget.CardView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import `in`.sminnovations.smiadmin.R
import `in`.sminnovations.smiadmin.data.AttendanceData
@@ -13,8 +17,9 @@ class AttendanceAdapter : RecyclerView.Adapter<AttendanceAdapter.ViewHolder>() {
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val dateTextView: TextView = itemView.findViewById(R.id.textViewDate)
val checkInTextView: TextView = itemView.findViewById(R.id.textViewCheckIn)
val checkOutTextView: TextView = itemView.findViewById(R.id.textViewCheckOut)
val checkInsLayout: LinearLayout = itemView.findViewById(R.id.layoutCheckIns)
val expandableLayout: LinearLayout = itemView.findViewById(R.id.expandableLayout)
val cardView: CardView = itemView.findViewById(R.id.cardView)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
@@ -26,24 +31,164 @@ class AttendanceAdapter : RecyclerView.Adapter<AttendanceAdapter.ViewHolder>() {
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val attendance = attendanceList[position]
holder.dateTextView.text = attendance.date
holder.checkInTextView.text = "Check In: ${attendance.checkIn}"
holder.checkOutTextView.text = "Check Out: ${attendance.checkOut}"
// Clear previous views
holder.checkInsLayout.removeAllViews()
// Display all attendance records
displayAttendanceRecords(holder.itemView.context, holder, attendance)
// Set up expansion/collapse
holder.expandableLayout.apply {
visibility = if (attendance.isExpanded) View.VISIBLE else View.GONE
// Add animation for smooth expansion/collapse
alpha = if (attendance.isExpanded) 1.0f else 0.0f
animate()
.alpha(if (attendance.isExpanded) 1.0f else 0.0f)
.setDuration(200)
.start()
}
holder.cardView.setOnClickListener {
// Toggle expansion state
val wasExpanded = attendance.isExpanded
attendance.isExpanded = !wasExpanded
// Animate the change
if (!wasExpanded) {
holder.expandableLayout.visibility = View.VISIBLE
holder.expandableLayout.alpha = 0.0f
holder.expandableLayout.animate()
.alpha(1.0f)
.setDuration(200)
.start()
} else {
holder.expandableLayout.animate()
.alpha(0.0f)
.setDuration(200)
.withEndAction {
if (!attendance.isExpanded) {
holder.expandableLayout.visibility = View.GONE
}
}
.start()
}
notifyItemChanged(position)
}
}
override fun getItemCount() = attendanceList.size
// In AttendanceAdapter.kt
fun updateAttendance(newAttendance: List<AttendanceData>) {
val diffCallback = AttendanceDiffCallback(attendanceList, newAttendance)
val diffResult = DiffUtil.calculateDiff(diffCallback)
attendanceList.clear()
// Convert date string to comparable format and sort
attendanceList.addAll(newAttendance.sortedByDescending {
// Convert dd-MM-yyyy to comparable format
val parts = it.date.split("-")
val day = parts[0].toInt()
val month = parts[1].toInt()
val year = parts[2].toInt()
String.format("%04d%02d%02d", year, month, day)
})
notifyDataSetChanged()
attendanceList.addAll(newAttendance.sortedByDescending { it.getComparableDate() })
diffResult.dispatchUpdatesTo(this)
}
private fun displayAttendanceRecords(
context: Context,
holder: ViewHolder,
attendance: AttendanceData
) {
// Create a set of all unique check-in keys
val allCheckInKeys = attendance.checkIns.keys
// Handle numbered check-ins
for (i in 1..attendance.checkIns.size) {
val checkInKey = "checkIn$i"
val checkOutKey = "checkOut$i"
if (attendance.checkIns.containsKey(checkInKey)) {
addCheckInPair(
context,
holder,
checkInKey,
checkOutKey,
attendance.checkIns[checkInKey]!!,
attendance.checkOuts[checkOutKey]
)
}
}
// Handle non-numbered check-ins
allCheckInKeys
.filter { !it.matches(Regex("checkIn\\d+")) }
.forEach { checkInKey ->
val baseKey = checkInKey.replace("checkIn", "")
val checkOutKey = "checkOut$baseKey"
addCheckInPair(
context,
holder,
checkInKey,
checkOutKey,
attendance.checkIns[checkInKey]!!,
attendance.checkOuts[checkOutKey]
)
}
}
private fun addCheckInPair(
context: Context,
holder: ViewHolder,
checkInKey: String,
checkOutKey: String,
checkInValue: String,
checkOutValue: String?
) {
val pairLayout = LayoutInflater.from(context)
.inflate(R.layout.item_checkin_checkout_pair, holder.checkInsLayout, false)
val checkInView = pairLayout.findViewById<TextView>(R.id.textViewCheckIn)
val checkOutView = pairLayout.findViewById<TextView>(R.id.textViewCheckOut)
// Display the original key name for non-standard keys
val checkInLabel = if (checkInKey.matches(Regex("checkIn\\d+"))) {
"Check In"
} else {
"Check In (${checkInKey.replace("checkIn", "")})"
}
val checkOutLabel = if (checkOutKey.matches(Regex("checkOut\\d+"))) {
"Check Out"
} else {
"Check Out (${checkOutKey.replace("checkOut", "")})"
}
checkInView.text = "$checkInLabel: $checkInValue"
checkOutView.text = "$checkOutLabel: ${checkOutValue ?: "Pending"}"
holder.checkInsLayout.addView(pairLayout)
}
}
// DiffUtil callback for efficient updates
private class AttendanceDiffCallback(
private val oldList: List<AttendanceData>,
private val newList: List<AttendanceData>
) : DiffUtil.Callback() {
override fun getOldListSize() = oldList.size
override fun getNewListSize() = newList.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldList[oldItemPosition].date == newList[newItemPosition].date
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldList[oldItemPosition] == newList[newItemPosition]
}
}
// Extension function for AttendanceData to get comparable date
private fun AttendanceData.getComparableDate(): String {
val parts = date.split("-")
val day = parts[0].toInt()
val month = parts[1].toInt()
val year = parts[2].toInt()
return String.format("%04d%02d%02d", year, month, day)
}

View File

@@ -83,7 +83,7 @@ class AddEmployeeFragment : Fragment() {
}
private fun generateEmployeeId(name: String, phone: String): String {
val namePrefix = name.take(4).uppercase()
val namePrefix = name.take(4).lowercase()
val phoneSuffix = phone.takeLast(4)
return "$namePrefix$phoneSuffix"
}
@@ -136,16 +136,29 @@ class AddEmployeeFragment : Fragment() {
val phone = binding.etEmployeePhone.text.toString().trim()
val employeeId = generateEmployeeId(name, phone)
val employee = Employee(
Id = employeeId,
Name = name,
Email = binding.etEmployeeEmail.text.toString().trim(),
Mobile = phone,
Designation = binding.etEmployeePosition.text.toString().trim(),
dateOfBirth = dateFormatter.format(dateOfBirth?.time ?: Date()),
DateOfJoining = dateFormatter.format(dateOfJoining?.time ?: Date()),
CreatedAt = dateFormatter.format(Date()), // Current date in dd/MM/yyyy format
from = binding.etEmployeeFrom.text.toString().trim(),
// val employee = Employee(
// Id = employeeId,
// Name = name,
// Email = binding.etEmployeeEmail.text.toString().trim(),
// Mobile = phone,
// Designation = binding.etEmployeePosition.text.toString().trim(),
// dateOfBirth = dateFormatter.format(dateOfBirth?.time ?: Date()),
// DateOfJoining = dateFormatter.format(dateOfJoining?.time ?: Date()),
// CreatedAt = dateFormatter.format(Date()), // Current date in dd/MM/yyyy format
// from = binding.etEmployeeFrom.text.toString().trim(),
// )
val employee = hashMapOf(
"Id" to employeeId,
"Name" to name,
"EmployeeStatus" to true,
"Email" to binding.etEmployeeEmail.text.toString().trim(),
"Mobile" to phone,
"Designation" to binding.etEmployeePosition.text.toString().trim(),
"dateOfBirth" to dateFormatter.format(dateOfBirth?.time ?: Date()),
"DateOfJoining" to dateFormatter.format(dateOfJoining?.time ?: Date()),
"CreatedAt" to dateFormatter.format(Date()),
"from" to binding.etEmployeeFrom.text.toString().trim(),
"role" to "user"
)
db.collection("employees")

View File

@@ -23,12 +23,16 @@ import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import `in`.sminnovations.smiadmin.data.Constant.STORAGE_PERMISSION_CODE
import `in`.sminnovations.smiadmin.R
import `in`.sminnovations.smiadmin.ui.utils.DailyMonthYearPickerDialog
import `in`.sminnovations.smiadmin.ui.utils.ExportTask
import `in`.sminnovations.smiadmin.ui.utils.MonthYearPickerDialog
import `in`.sminnovations.smiadmin.ui.utils.dailyReportExport
import kotlinx.coroutines.Job
import java.util.Calendar
class HomeFragment : Fragment() {
private lateinit var exportTask: ExportTask
private lateinit var dailyReportExport: dailyReportExport
private var currentExcelTask: Job? = null
override fun onCreateView(
@@ -37,9 +41,10 @@ class HomeFragment : Fragment() {
savedInstanceState: Bundle?
): View? {
exportTask = ExportTask(this)
dailyReportExport = dailyReportExport(this)
return inflater.inflate(R.layout.fragment_home, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@@ -63,6 +68,13 @@ class HomeFragment : Fragment() {
requestStoragePermission()
}
}
view.findViewById<CardView>(R.id.cardDownloadDailyReport).setOnClickListener{
if(checkStoragePermission()){
showDailyMonthYearPicker()
}else{
requestStoragePermission()
}
}
}
private fun showMonthYearPicker() {
@@ -73,6 +85,15 @@ class HomeFragment : Fragment() {
}
}.show(parentFragmentManager, "MonthYearPicker")
}
private fun showDailyMonthYearPicker(){
DailyMonthYearPickerDialog().apply {
setOnDateSelectedListener {
showDailyReportDialog(it,requireContext())
}
}.show(parentFragmentManager,"Daily Date Picker")
}
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)
@@ -112,6 +133,19 @@ class HomeFragment : Fragment() {
progressDialog.dismiss()
}
private fun downloadAttendanceDailyReport(date : Calendar, source: String) {
currentExcelTask?.cancel()
val progressDialog = ProgressDialog(requireContext()).apply {
setMessage("Generating report...")
setCancelable(false)
show()
}
dailyReportExport.exportAttendance(date =date,source =source)
progressDialog.dismiss()
}
private fun checkStoragePermission(): Boolean {
return if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
ContextCompat.checkSelfPermission(
@@ -154,6 +188,7 @@ class HomeFragment : Fragment() {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// Permission granted, now show the picker
showMonthYearPicker()
} else {
Toast.makeText(
requireContext(),
@@ -163,8 +198,36 @@ class HomeFragment : Fragment() {
}
}
}
private fun showDailyReportDialog(date : Calendar, context: Context) {
val dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_report_source, null)
val dialog = AlertDialog.Builder(context)
.setView(dialogView)
.setCancelable(true)
.create()
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) {
downloadAttendanceDailyReport(date, selectedSource)
dialog.dismiss()
} else {
Toast.makeText(context, "Please select a source", Toast.LENGTH_SHORT).show()
}
}
dialog.show()
}
override fun onDestroy() {
super.onDestroy()
dailyReportExport.cleanup()
exportTask.cleanup()
}
}

View File

@@ -2,6 +2,7 @@ package `in`.sminnovations.smiadmin.ui.fragments.attendance
import android.R
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -123,13 +124,38 @@ class AttendanceDetailFragment : Fragment() {
// Only include records for selected month and year
if (docMonth == month && docYear == year) {
AttendanceData(
date = doc.id,
checkIn = doc.getString("checkIn") ?: "N/A",
checkOut = doc.getString("checkOut") ?: "N/A"
)
// Get all fields from the document
val data = doc.data ?: return@mapNotNull null
// Create maps to store check-ins and check-outs
val checkIns = mutableMapOf<String, String>()
val checkOuts = mutableMapOf<String, String>()
// Iterate through all fields in the document
data.forEach { (key, value) ->
when {
// Match checkIn1, checkIn2, etc.
key.startsWith("checkIn") -> {
checkIns[key] = (value as? String) ?: "N/A"
}
// Match checkOut1, checkOut2, etc.
key.startsWith("checkOut") -> {
checkOuts[key] = (value as? String) ?: "N/A"
}
}
}
// Only create AttendanceData if there's at least one check-in
if (checkIns.isNotEmpty()) {
AttendanceData(
date = doc.id,
checkIns = checkIns,
checkOuts = checkOuts
)
} else null
} else null
} catch (e: Exception) {
Log.e("AttendanceMapping", "Error mapping document: ${doc.id}", e)
null
}
}

View File

@@ -0,0 +1,49 @@
package `in`.sminnovations.smiadmin.ui.utils
import android.app.AlertDialog
import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.widget.CalendarView
import androidx.fragment.app.DialogFragment
import `in`.sminnovations.smiadmin.R
import java.util.Calendar
class DailyMonthYearPickerDialog : DialogFragment() {
private var onDateSelected: ((Calendar) -> Unit)? = null
private var selectedDate: Calendar = Calendar.getInstance()
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
// Inflate the custom layout
val view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_date_month_year_picker, null)
// Initialize CalendarView
val calendarPicker = view.findViewById<CalendarView>(R.id.calendarViewPicker)
// Set default date
calendarPicker.date = selectedDate.timeInMillis
// Listen for date changes
calendarPicker.setOnDateChangeListener { _, year, month, dayOfMonth ->
selectedDate.set(Calendar.YEAR, year)
selectedDate.set(Calendar.MONTH, month)
selectedDate.set(Calendar.DAY_OF_MONTH, dayOfMonth)
}
// Build the Dialog
val dialog = AlertDialog.Builder(requireContext())
.setView(view)
.setTitle("Select Date for the Report")
.setPositiveButton("Generate Report") { _, _ ->
// Pass the selected date back through the callback
onDateSelected?.invoke(selectedDate)
}
.setNegativeButton("Cancel", null)
.create()
return dialog
}
fun setOnDateSelectedListener(listener: (Calendar) -> Unit) {
onDateSelected = listener
}
}

View File

@@ -0,0 +1,20 @@
package `in`.sminnovations.smiadmin.ui.utils
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Build
class NetworkUtils(private val context: Context) {
fun isInternetAvailable(): Boolean {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val network = connectivityManager.activeNetwork ?: return false
val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false
return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
} else {
val networkInfo = connectivityManager.activeNetworkInfo ?: return false
return networkInfo.isConnected
}
}
}

View File

@@ -0,0 +1,523 @@
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 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.Calendar
import java.util.Locale
class PDFGenerator_daily(
private val context: Context,
private val companyName: String
){
private lateinit var document: PdfDocument
private lateinit var currentPage: PdfDocument.Page
private lateinit var canvas: android.graphics.Canvas
private var currentY: Float = 0f
// Constants for page layout
private val pageWidth = 595f // A4 width
private val pageHeight = 842f // A4 height
private val margin = 10f
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
// Data class for attendance record
data class AttendanceRecord(
val checkInTimes: List<String>,
val checkOutTimes: List<String>,
val isCompleted: Boolean
)
suspend fun generateDailyReport(
employees: List<Employee>,
selectedDate: Calendar,
source: String
): File {
document = PdfDocument()
createNewPage()
// Create paint objects
val titlePaint = Paint().apply {
textSize = 32f
typeface = Typeface.create("sans-serif-light", Typeface.BOLD)
color = primaryColor
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 notePaint = Paint().apply {
textSize = 10f
typeface = Typeface.MONOSPACE
color = secondaryColor
textAlign = Paint.Align.CENTER
}
val tableHeaderPaint = Paint().apply {
textSize = 14f
typeface = Typeface.create("sans-serif-medium", Typeface.BOLD)
color = Color.WHITE
}
val regularPaint = Paint().apply {
textSize = 12f
typeface = Typeface.create("sans-serif", Typeface.NORMAL)
color = textColor
}
val highlightPaint = Paint().apply {
textSize = 12f
typeface = Typeface.create("sans-serif-medium", Typeface.BOLD)
color = accentColor
}
// Format date for display and Firebase query
val dateFormat = SimpleDateFormat("dd MMMM yyyy", Locale.getDefault())
val formattedDate = dateFormat.format(selectedDate.time)
val yearStr = selectedDate.get(Calendar.YEAR).toString()
val monthStr = String.format("%02d", selectedDate.get(Calendar.MONTH) + 1)
val dayStr = String.format("%02d", selectedDate.get(Calendar.DAY_OF_MONTH))
val firebaseDateKey = "$dayStr-$monthStr-$yearStr"
//
drawTopDisclaimer(notePaint)
// Draw report header
drawReportHeader(formattedDate, titlePaint, subtitlePaint)
// Draw table headers
drawTableHeaders(tableHeaderPaint)
// Process each employee
employees.forEach { employee ->
if (currentY > pageHeight - 100) {
finishCurrentPage()
createNewPage()
drawTableHeaders(tableHeaderPaint)
}
// Get attendance data for this employee for the selected date
val attendanceDoc = Firebase.firestore
.collection("attendance_${employee.Id}")
.document(firebaseDateKey)
.get()
.await()
if (attendanceDoc.exists()) {
// Extract check-in and check-out times
val checkInTimes = mutableListOf<String>()
val checkOutTimes = mutableListOf<String>()
val isCompleted = attendanceDoc.getBoolean("DayCompleted") ?: false
// Get all check-in times
var i = 1
while (true) {
val checkInKey = "checkIn$i"
val checkInTime = attendanceDoc.getString(checkInKey)
if (checkInTime != null) {
checkInTimes.add(checkInTime)
i++
} else {
break
}
}
// Get all check-out times
i = 1
while (true) {
val checkOutKey = "checkOut$i"
val checkOutTime = attendanceDoc.getString(checkOutKey)
if (checkOutTime != null) {
checkOutTimes.add(checkOutTime)
i++
} else {
break
}
}
val attendanceRecord = AttendanceRecord(
checkInTimes = checkInTimes,
checkOutTimes = checkOutTimes,
isCompleted = isCompleted
)
drawEmployeeAttendanceRow(employee, attendanceRecord, regularPaint, highlightPaint)
} else {
// Employee was absent
drawEmployeeAbsentRow(employee, regularPaint, highlightPaint)
}
}
// Add footer with page numbers
drawFooter(regularPaint)
// Finish and save document
finishCurrentPage()
return saveDocument(selectedDate)
}
private fun drawReportHeader(
formattedDate: String,
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
canvas.drawText(
"Daily Attendance Report",
pageWidth/2,
105f,
subtitlePaint
)
canvas.drawText(
formattedDate,
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 += 150f
canvas.drawText("Status", xPos, currentY + 30f, headerPaint)
xPos += 80f
canvas.drawText("Check-in", xPos, currentY + 30f, headerPaint)
xPos += 150f
canvas.drawText("Check-out", xPos, currentY + 30f, headerPaint)
xPos += 110f
canvas.drawText("Hours", xPos, currentY + 30f, headerPaint)
currentY += headerHeight + 5f
}
private fun drawEmployeeAttendanceRow(
employee: Employee,
attendanceRecord: AttendanceRecord,
regularPaint: Paint,
highlightPaint: Paint
) {
// Calculate row height based on number of check-ins/outs
val entriesCount = maxOf(attendanceRecord.checkInTimes.size, attendanceRecord.checkOutTimes.size)
val rowHeight = 40f + (entriesCount * 20f)
// 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 + 25f,
regularPaint
)
// Draw status
val status = if (attendanceRecord.isCompleted) "Present" else "Incomplete"
val statusPaint = if (attendanceRecord.isCompleted) {
Paint().apply {
textSize = 12f
typeface = Typeface.create("sans-serif-medium", Typeface.BOLD)
color = Color.rgb(46, 204, 113) // Green for present
}
} else {
Paint().apply {
textSize = 12f
typeface = Typeface.create("sans-serif-medium", Typeface.BOLD)
color = Color.rgb(92, 75, 242) // Purple for incomplete
}
}
canvas.drawText(
status,
margin + 165f,
currentY + 25f,
statusPaint
)
// Draw check-in times
var checkInY = currentY + 25f
attendanceRecord.checkInTimes.forEachIndexed { index, time ->
// Extract only the time portion for display
val simplifiedTime = try {
val parsedTime = SimpleDateFormat("dd MMMM yyyy hh:mm:ss a", Locale.getDefault()).parse(time)
SimpleDateFormat("hh:mm:ss a", Locale.getDefault()).format(parsedTime)
} catch (e: Exception) {
time // Fallback if parsing fails
}
canvas.drawText(
"$simplifiedTime",
margin + 245f,
checkInY,
regularPaint
)
checkInY += 20f
}
// Draw check-out times
var checkOutY = currentY + 25f
attendanceRecord.checkOutTimes.forEachIndexed { index, time ->
// Extract only the time portion for display
val simplifiedTime = try {
val parsedTime = SimpleDateFormat("dd MMMM yyyy hh:mm:ss a", Locale.getDefault()).parse(time)
SimpleDateFormat("hh:mm:ss a", Locale.getDefault()).format(parsedTime)
} catch (e: Exception) {
time // Fallback if parsing fails
}
canvas.drawText(
"$simplifiedTime",
margin + 395f,
checkOutY,
regularPaint
)
checkOutY += 20f
}
// Calculate and draw total hours
val totalHours = calculateTotalHours(attendanceRecord.checkInTimes, attendanceRecord.checkOutTimes)
canvas.drawText(
totalHours,
margin + 505f,
currentY + 25f,
highlightPaint
)
currentY += rowHeight + 10f
}
private fun drawEmployeeAbsentRow(
employee: Employee,
regularPaint: Paint,
highlightPaint: Paint
) {
val rowHeight = 40f
// Draw row background with a slight red tint for absent
canvas.drawRoundRect(
margin, currentY,
pageWidth - margin,
currentY + rowHeight,
4f, 4f,
Paint().apply {
color = Color.rgb(245, 233, 233) // Light red tint
}
)
// Draw employee name
canvas.drawText(
employee.Name,
margin + 15f,
currentY + 25f,
regularPaint
)
// Draw absent status
val absentPaint = Paint().apply {
textSize = 12f
typeface = Typeface.create("sans-serif-medium", Typeface.BOLD)
color = Color.rgb(231, 76, 60) // Red for absent
}
canvas.drawText(
"Absent",
margin + 165f,
currentY + 25f,
absentPaint
)
// Draw dashes for check-in and check-out
canvas.drawText(
"",
margin + 245f,
currentY + 25f,
regularPaint
)
canvas.drawText(
"",
margin + 395f,
currentY + 25f,
regularPaint
)
// Draw zero hours
canvas.drawText(
"0.00",
margin + 505f,
currentY + 25f,
highlightPaint
)
currentY += rowHeight + 10f
}
private fun calculateTotalHours(checkInTimes: List<String>, checkOutTimes: List<String>): String {
var totalMinutes = 0f
val dateFormat = SimpleDateFormat("dd MMMM yyyy hh:mm:ss a", Locale.getDefault())
// Calculate time for each check-in/check-out pair
for (i in 0 until minOf(checkInTimes.size, checkOutTimes.size)) {
try {
val checkInTime = dateFormat.parse(checkInTimes[i])
val checkOutTime = dateFormat.parse(checkOutTimes[i])
if (checkInTime != null && checkOutTime != null) {
// Calculate difference in minutes
val diffMillis = checkOutTime.time - checkInTime.time
val diffMinutes = diffMillis / (1000 * 60)
totalMinutes += diffMinutes
}
} catch (e: Exception) {
// Skip if there's an error parsing the date
continue
}
}
// Convert total minutes to hours and minutes
val hours = totalMinutes.toInt() / 60
val minutes = totalMinutes.toInt() % 60
// Format as "HH:MM"
return String.format("%d:%02d", hours, minutes)
}
private fun drawFooter(paint: Paint, ) {
paint.textAlign = Paint.Align.CENTER
paint.textSize = 10f
canvas.drawText(
"Page ${pageNumber -1}",
pageWidth/2,
pageHeight - margin/2,
paint
)
}
private fun drawTopDisclaimer(notePaint: Paint){
val currentDate = SimpleDateFormat("dd MMMM yyyy HH:mm", Locale.getDefault())
.format(Calendar.getInstance().time)
canvas.drawText(
"This Report was generated by AdminApp on: $currentDate ",
pageWidth/2,
20f,
notePaint
)
}
private fun saveDocument(selectedDate: Calendar): File {
val dateStr = SimpleDateFormat("yyyy_MM_dd", Locale.getDefault())
.format(selectedDate.time)
val fileName = "Daily_Attendance_Report_$dateStr.pdf"
val file = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
fileName
)
FileOutputStream(file).use { outputStream ->
document.writeTo(outputStream)
}
document.close()
return file
}
private fun createNewPage() {
val pageInfo = PdfDocument.PageInfo.Builder(
pageWidth.toInt(),
pageHeight.toInt(),
pageNumber++
).create()
currentPage = document.startPage(pageInfo)
canvas = currentPage.canvas
currentY = margin
}
private fun finishCurrentPage() {
document.finishPage(currentPage)
}
}

View File

@@ -0,0 +1,183 @@
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.firestore
import `in`.sminnovations.smiadmin.data.Constant
import `in`.sminnovations.smiadmin.data.Employee
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.tasks.await
import kotlinx.coroutines.withContext
import java.io.File
import java.io.IOException
import java.util.Calendar
class dailyReportExport(private val fragment: Fragment) {
private val db = Firebase.firestore
private var currentExcelTask: Job? = null
// 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(date : Calendar, source: String) {
if (!checkStoragePermission()) {
ActivityCompat.requestPermissions(
fragment.requireActivity(),
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
STORAGE_PERMISSION_CODE
)
return
}
// Cancel any existing task
currentExcelTask?.cancel()
val progressDialog = ProgressDialog(fragment.requireContext()).apply {
setMessage("Generating attendance report...")
setCancelable(false)
show()
}
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") ?: doc.getString("id") ?: "",
Name = doc.getString("Name") ?: doc.getString("name") ?: "",
)
}.sortedBy { it.Name }
withContext(Dispatchers.IO) {
val pdfGenerator = PDFGenerator_daily(fragment.requireContext(), company)
pdfGenerator.generateDailyReport(employees, selectedDate = date,source = source)
}.also { pdfFile ->
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 - ")
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) {
withContext(Dispatchers.Main) {
progressDialog.dismiss()
Toast.makeText(
fragment.context,
"Error: ${e.localizedMessage ?: "Unknown error occurred"}",
Toast.LENGTH_LONG
).show()
}
e.printStackTrace()
}
}
}
fun cleanup() {
currentExcelTask?.cancel()
}
companion object {
private const val STORAGE_PERMISSION_CODE = 101
}
}

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="M7,10l5,5 5,-5z"/>
</vector>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/dailyExport"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autoSizeTextType="uniform"
android:background="#E49D9D"
android:text="Make sure you are Exporting Report at the end of the day"
android:textSize="20sp" />
<CalendarView
android:id="@+id/calendarViewPicker"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>

View File

@@ -18,7 +18,7 @@
android:id="@+id/spinnerMonth"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginStart="5dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/textViewEmployeeName" />
@@ -26,7 +26,7 @@
android:id="@+id/spinnerYear"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginStart="5dp"
app:layout_constraintStart_toEndOf="@id/spinnerMonth"
app:layout_constraintTop_toTopOf="@id/spinnerMonth" />

View File

@@ -99,6 +99,34 @@
android:text="Download attendance reports" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:id="@+id/cardDownloadDailyReport"
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="Download Daily Report"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Download daily attendance reports" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:id="@+id/cardAddEmployee"
android:layout_width="match_parent"

View File

@@ -2,79 +2,80 @@
<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_height="fill_parent"
android:layout_gravity="center"
>
android:orientation="vertical">
<LinearLayout
android:id="@+id/parentLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal|center_vertical"
android:layout_marginTop="100.dp"
android:background="@drawable/smi_logo"
android:layout_margin="15dp"
android:gravity="center"
android:orientation="vertical"
android:padding="16dp"
android:visibility="visible"
android:gravity="center">
</LinearLayout>
android:visibility="visible" />
<LinearLayout
android:id="@+id/childLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:padding="16dp"
android:visibility="gone"
android:gravity="center">
android:visibility="gone">
<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"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
android:text="Admin Login"
android:textSize="24sp"
android:textStyle="bold" />
<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"
<com.google.android.material.textfield.TextInputLayout
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>
android:layout_marginBottom="16dp">
<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/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.TextInputEditText
android:id="@+id/etPassword"
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Password"
android:inputType="textPassword"/>
</com.google.android.material.textfield.TextInputLayout>
android:layout_marginBottom="24dp"
app:passwordToggleEnabled="true">
<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"/>
<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>
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:visibility="gone"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnLogin"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp"
android:text="Login" />
</LinearLayout>
<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

@@ -1,42 +1,71 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
<androidx.cardview.widget.CardView
android:id="@+id/cardView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:cardCornerRadius="8dp"
app:cardElevation="4dp">
app:cardElevation="4dp"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
android:orientation="vertical">
<TextView
android:id="@+id/textViewDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textStyle="bold" />
<LinearLayout
<!-- Header Layout -->
<RelativeLayout
android:id="@+id/headerLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal">
android:padding="16dp"
android:background="?android:attr/selectableItemBackground">
<TextView
android:id="@+id/textViewCheckIn"
android:layout_width="0dp"
android:id="@+id/textViewDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1" />
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:textSize="18sp"
android:textStyle="bold"
/>
<TextView
android:id="@+id/textViewCheckOut"
android:layout_width="0dp"
<ImageView
android:id="@+id/imageViewArrow"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:src="@drawable/baseline_arrow_drop_down_24"
android:rotation="0" />
</RelativeLayout>
<!-- Divider -->
<View
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:attr/listDivider"
android:visibility="gone" />
<!-- Expandable Content -->
<LinearLayout
android:id="@+id/expandableLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone"
android:padding="16dp">
<LinearLayout
android:id="@+id/layoutCheckIns"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1" />
android:orientation="vertical" />
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
</androidx.cardview.widget.CardView>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<TextView
android:id="@+id/textViewCheckIn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@android:color/black" />
<TextView
android:id="@+id/textViewCheckOut"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textColor="@android:color/black" />
</LinearLayout>