Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ed17b3e07 | ||
|
|
c9db1cb727 | ||
|
|
5489aeb588 | ||
|
|
1cebe79071 | ||
|
|
aed649bf7d | ||
|
|
e2e5733d4b | ||
|
|
419e628d06 | ||
|
|
b269993e13 | ||
|
|
53149d0361 | ||
|
|
811f71049d | ||
|
|
9c3156a85a | ||
|
|
2867ecce4f | ||
|
|
6c90a1a183 | ||
|
|
43d6979fac | ||
|
|
43317574e9 | ||
|
|
b7d0fd32a9 | ||
|
|
a72c062b96 | ||
|
|
304f2cbe60 |
154
.idea/other.xml
generated
154
.idea/other.xml
generated
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
81
app/src/main/java/in/sminnovations/smiadmin/BaseActivity.kt
Normal file
81
app/src/main/java/in/sminnovations/smiadmin/BaseActivity.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
23
app/src/main/res/layout/dialog_date_month_year_picker.xml
Normal file
23
app/src/main/res/layout/dialog_date_month_year_picker.xml
Normal 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>
|
||||
@@ -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" />
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
21
app/src/main/res/layout/item_checkin_checkout_pair.xml
Normal file
21
app/src/main/res/layout/item_checkin_checkout_pair.xml
Normal 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>
|
||||
Reference in New Issue
Block a user