generate pdf code added
This commit is contained in:
@@ -13,8 +13,8 @@ android {
|
||||
applicationId "in.sminnovations.smiadmin"
|
||||
minSdk 26
|
||||
targetSdk 34
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
versionCode 2
|
||||
versionName "1.1"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
package `in`.sminnovations.smiadmin.ui
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import com.google.firebase.Firebase
|
||||
import com.google.firebase.firestore.firestore
|
||||
import `in`.sminnovations.smiadmin.data.AttendanceData
|
||||
import `in`.sminnovations.smiadmin.data.Employee
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.tasks.await
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
class CSVGenerator(private val context: Context) {
|
||||
suspend fun generateAttendanceReport(
|
||||
employees: List<Employee>,
|
||||
selectedYear: Int,
|
||||
selectedMonth: Int
|
||||
): File {
|
||||
val fileName = "Attendance_${selectedYear}_${selectedMonth}.csv"
|
||||
|
||||
return withContext(Dispatchers.IO) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val contentValues = ContentValues().apply {
|
||||
put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
|
||||
put(MediaStore.MediaColumns.MIME_TYPE, "text/csv")
|
||||
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
|
||||
}
|
||||
|
||||
val uri = context.contentResolver.insert(
|
||||
MediaStore.Downloads.EXTERNAL_CONTENT_URI,
|
||||
contentValues
|
||||
) ?: throw IOException("Failed to create new MediaStore record.")
|
||||
|
||||
context.contentResolver.openOutputStream(uri)?.use { outputStream ->
|
||||
val writer = outputStream.bufferedWriter()
|
||||
|
||||
val dates = mutableSetOf<String>()
|
||||
val attendanceData = mutableMapOf<String, Map<String, AttendanceData>>()
|
||||
|
||||
// Format month with leading zero
|
||||
val monthStr = String.format("%02d", selectedMonth)
|
||||
|
||||
// Collect attendance data for each employee
|
||||
employees.forEach { employee ->
|
||||
// Get all documents and filter by month pattern in document ID
|
||||
val snapshot = Firebase.firestore
|
||||
.collection("attendance_${employee.Id}")
|
||||
.get()
|
||||
.await()
|
||||
|
||||
val employeeAttendance = snapshot.documents
|
||||
.filter { doc ->
|
||||
// Filter documents that match the pattern DD-MM-YYYY for selected month
|
||||
doc.id.matches(Regex("\\d{2}-$monthStr-$selectedYear"))
|
||||
}
|
||||
.associate { doc ->
|
||||
dates.add(doc.id)
|
||||
doc.id to AttendanceData(
|
||||
date = doc.id,
|
||||
checkIn = doc.getString("checkIn") ?: "N/A",
|
||||
checkOut = doc.getString("checkOut") ?: "N/A"
|
||||
)
|
||||
}
|
||||
attendanceData[employee.Id] = employeeAttendance
|
||||
}
|
||||
|
||||
// Sort dates
|
||||
val sortedDates = dates.sortedBy { date ->
|
||||
val parts = date.split("-")
|
||||
val day = parts[0].toInt()
|
||||
String.format("%02d", day)
|
||||
}
|
||||
|
||||
// Write header
|
||||
val headerRow = listOf("Employee ID", "Name") + sortedDates
|
||||
writer.write(headerRow.joinToString(","))
|
||||
writer.newLine()
|
||||
|
||||
// Write employee data
|
||||
employees.forEach { employee ->
|
||||
val rowData = mutableListOf<String>()
|
||||
rowData.add(employee.Id)
|
||||
rowData.add(employee.Name)
|
||||
|
||||
val employeeAttendance = attendanceData[employee.Id] ?: emptyMap()
|
||||
sortedDates.forEach { date ->
|
||||
val attendance = employeeAttendance[date]
|
||||
val cellValue = when {
|
||||
attendance == null -> "Absent"
|
||||
attendance.checkOut == "N/A" -> attendance.checkIn
|
||||
else -> "${attendance.checkIn} - ${attendance.checkOut}"
|
||||
}
|
||||
rowData.add("\"${cellValue.replace("\"", "\"\"")}\"")
|
||||
}
|
||||
|
||||
writer.write(rowData.joinToString(","))
|
||||
writer.newLine()
|
||||
}
|
||||
|
||||
writer.flush()
|
||||
}
|
||||
|
||||
File(Environment.getExternalStorageDirectory(), "Download/$fileName")
|
||||
} else {
|
||||
val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
||||
val file = File(downloadsDir, fileName)
|
||||
|
||||
file.bufferedWriter().use { writer ->
|
||||
val dates = mutableSetOf<String>()
|
||||
val attendanceData = mutableMapOf<String, Map<String, AttendanceData>>()
|
||||
|
||||
val monthStr = String.format("%02d", selectedMonth)
|
||||
|
||||
employees.forEach { employee ->
|
||||
val snapshot = Firebase.firestore
|
||||
.collection("attendance_${employee.Id}")
|
||||
.get()
|
||||
.await()
|
||||
|
||||
val employeeAttendance = snapshot.documents
|
||||
.filter { doc ->
|
||||
doc.id.matches(Regex("\\d{2}-$monthStr-$selectedYear"))
|
||||
}
|
||||
.associate { doc ->
|
||||
dates.add(doc.id)
|
||||
doc.id to AttendanceData(
|
||||
date = doc.id,
|
||||
checkIn = doc.getString("checkIn") ?: "N/A",
|
||||
checkOut = doc.getString("checkOut") ?: "N/A"
|
||||
)
|
||||
}
|
||||
attendanceData[employee.Id] = employeeAttendance
|
||||
}
|
||||
|
||||
val sortedDates = dates.sortedBy { date ->
|
||||
val parts = date.split("-")
|
||||
val day = parts[0].toInt()
|
||||
String.format("%02d", day)
|
||||
}
|
||||
|
||||
val headerRow = listOf("Employee ID", "Name") + sortedDates
|
||||
writer.write(headerRow.joinToString(","))
|
||||
writer.newLine()
|
||||
|
||||
employees.forEach { employee ->
|
||||
val rowData = mutableListOf<String>()
|
||||
rowData.add(employee.Id)
|
||||
rowData.add(employee.Name)
|
||||
|
||||
val employeeAttendance = attendanceData[employee.Id] ?: emptyMap()
|
||||
sortedDates.forEach { date ->
|
||||
val attendance = employeeAttendance[date]
|
||||
val cellValue = when {
|
||||
attendance == null -> "Absent"
|
||||
attendance.checkOut == "N/A" -> attendance.checkIn
|
||||
else -> "${attendance.checkIn} - ${attendance.checkOut}"
|
||||
}
|
||||
rowData.add("\"${cellValue.replace("\"", "\"\"")}\"")
|
||||
}
|
||||
|
||||
writer.write(rowData.joinToString(","))
|
||||
writer.newLine()
|
||||
}
|
||||
|
||||
writer.flush()
|
||||
}
|
||||
|
||||
file
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
82
app/src/main/java/in/sminnovations/smiadmin/ui/ExportTask.kt
Normal file
82
app/src/main/java/in/sminnovations/smiadmin/ui/ExportTask.kt
Normal file
@@ -0,0 +1,82 @@
|
||||
package `in`.sminnovations.smiadmin.ui
|
||||
|
||||
import android.app.ProgressDialog
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.gms.common.internal.Constants
|
||||
import com.google.firebase.Firebase
|
||||
import com.google.firebase.firestore.firestore
|
||||
import `in`.sminnovations.smiadmin.data.Employee
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.tasks.await
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class ExportTask(private val fragment: Fragment) {
|
||||
private val companyName = "ShanMukha Innovations"
|
||||
private val db = Firebase.firestore
|
||||
private var currentExcelTask: Job? = null
|
||||
|
||||
fun exportAttendance(year: Int, month: Int) {
|
||||
// Cancel any existing task
|
||||
currentExcelTask?.cancel()
|
||||
|
||||
// Show progress dialog
|
||||
val progressDialog = ProgressDialog(fragment.requireContext()).apply {
|
||||
setMessage("Generating attendance report...")
|
||||
setCancelable(false)
|
||||
show()
|
||||
}
|
||||
|
||||
currentExcelTask = fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
// First get all employees
|
||||
val employeesSnapshot = withContext(Dispatchers.IO) {
|
||||
db.collection("employees")
|
||||
.whereEqualTo("EmployeeStatus", true)
|
||||
.get()
|
||||
.await()
|
||||
}
|
||||
|
||||
val employees = employeesSnapshot.documents.map { doc ->
|
||||
Employee(
|
||||
Id = doc.getString("Id") ?: "Unknown",
|
||||
Name = doc.getString("Name") ?: "Unknown"
|
||||
)
|
||||
}.sortedBy { it.Name }
|
||||
|
||||
// Switch to IO dispatcher for file operations
|
||||
withContext(Dispatchers.IO) {
|
||||
val pdfGenerator = PDFGenerator(fragment.requireContext(), companyName)
|
||||
pdfGenerator.generateAttendanceReport(employees, year, month)
|
||||
}.also { pdfFile ->
|
||||
// Switch back to Main dispatcher for UI updates
|
||||
withContext(Dispatchers.Main) {
|
||||
progressDialog.dismiss()
|
||||
Toast.makeText(
|
||||
fragment.context,
|
||||
"Report saved in Downloads folder: ${pdfFile.name}",
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
} 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()
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ import kotlinx.coroutines.tasks.await
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class HomeFragment : Fragment() {
|
||||
private val db = Firebase.firestore
|
||||
private lateinit var exportTask: ExportTask
|
||||
private val STORAGE_PERMISSION_CODE = 101
|
||||
private var currentExcelTask: Job? = null
|
||||
|
||||
@@ -42,6 +42,7 @@ class HomeFragment : Fragment() {
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
exportTask = ExportTask(this)
|
||||
return inflater.inflate(R.layout.fragment_home, container, false)
|
||||
}
|
||||
|
||||
@@ -81,39 +82,41 @@ class HomeFragment : Fragment() {
|
||||
setCancelable(false)
|
||||
show()
|
||||
}
|
||||
exportTask.exportAttendance(year, month)
|
||||
progressDialog.dismiss()
|
||||
|
||||
currentExcelTask = lifecycleScope.launch {
|
||||
try {
|
||||
// First get all employees
|
||||
val employeesSnapshot = withContext(Dispatchers.IO) {
|
||||
db.collection("employees").get().await()
|
||||
}
|
||||
|
||||
val employees = employeesSnapshot.documents.map { doc ->
|
||||
Employee(
|
||||
Id = doc.getString("Id") ?: "Unknown",
|
||||
Name = doc.getString("Name") ?: "Unknown"
|
||||
)
|
||||
}.sortedBy { it.Name }
|
||||
|
||||
// Generate CSV file
|
||||
val csvGenerator = CSVGenerator(requireContext())
|
||||
val csvFile = csvGenerator.generateAttendanceReport(employees, year, month)
|
||||
|
||||
progressDialog.dismiss()
|
||||
|
||||
// Show success message with file location
|
||||
Toast.makeText(
|
||||
context,
|
||||
"Report saved in Downloads folder: ${csvFile.name}",
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
|
||||
} catch (e: Exception) {
|
||||
progressDialog.dismiss()
|
||||
Toast.makeText(context, "Error: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
// currentExcelTask = lifecycleScope.launch {
|
||||
// try {
|
||||
// // First get all employees
|
||||
// val employeesSnapshot = withContext(Dispatchers.IO) {
|
||||
// db.collection("employees").get().await()
|
||||
// }
|
||||
//
|
||||
// val employees = employeesSnapshot.documents.map { doc ->
|
||||
// Employee(
|
||||
// Id = doc.getString("Id") ?: "Unknown",
|
||||
// Name = doc.getString("Name") ?: "Unknown"
|
||||
// )
|
||||
// }.sortedBy { it.Name }
|
||||
//
|
||||
// // Generate CSV file
|
||||
// val pdfGenerator = PDFGenerator(requireContext(), "ShanMukha Innovations")
|
||||
// val pdfFile = pdfGenerator.generateAttendanceReport(employees, year, month)
|
||||
//
|
||||
// progressDialog.dismiss()
|
||||
//
|
||||
// // Show success message with file location
|
||||
// Toast.makeText(
|
||||
// context,
|
||||
// "Report saved in Downloads folder: ${pdfFile.name}",
|
||||
// Toast.LENGTH_LONG
|
||||
// ).show()
|
||||
//
|
||||
// } catch (e: Exception) {
|
||||
// progressDialog.dismiss()
|
||||
// Toast.makeText(context, "Error: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
private fun checkStoragePermission(): Boolean {
|
||||
@@ -167,4 +170,8 @@ class HomeFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
exportTask.cleanup()
|
||||
}
|
||||
}
|
||||
222
app/src/main/java/in/sminnovations/smiadmin/ui/PDFGenerator.kt
Normal file
222
app/src/main/java/in/sminnovations/smiadmin/ui/PDFGenerator.kt
Normal file
@@ -0,0 +1,222 @@
|
||||
package `in`.sminnovations.smiadmin.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.pdf.PdfDocument
|
||||
import android.os.Environment
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Typeface
|
||||
import android.graphics.Color
|
||||
import com.google.firebase.Firebase
|
||||
import com.google.firebase.firestore.firestore
|
||||
import `in`.sminnovations.smiadmin.data.Employee
|
||||
import kotlinx.coroutines.tasks.await
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
class PDFGenerator(
|
||||
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
|
||||
private val startX = 40f
|
||||
private val pageHeight = 842f // A4 height
|
||||
private val pageWidth = 595f // A4 width
|
||||
private var pageNumber = 1
|
||||
|
||||
// Define colors
|
||||
private val primaryColor = Color.rgb(41, 128, 185) // Blue
|
||||
private val secondaryColor = Color.rgb(44, 62, 80) // Dark Blue
|
||||
private val borderColor = Color.rgb(189, 195, 199) // Light Gray
|
||||
private val backgroundColor = Color.rgb(236, 240, 241) // Very Light Gray
|
||||
|
||||
suspend fun generateAttendanceReport(
|
||||
employees: List<Employee>,
|
||||
selectedYear: Int,
|
||||
selectedMonth: Int
|
||||
): File {
|
||||
document = PdfDocument()
|
||||
createNewPage()
|
||||
|
||||
// Setup paints
|
||||
val headerPaint = Paint().apply {
|
||||
textSize = 28f
|
||||
textAlign = Paint.Align.CENTER
|
||||
typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
|
||||
color = primaryColor
|
||||
}
|
||||
|
||||
val subHeaderPaint = Paint().apply {
|
||||
textSize = 20f
|
||||
textAlign = Paint.Align.CENTER
|
||||
color = secondaryColor
|
||||
}
|
||||
|
||||
val tableHeaderPaint = Paint().apply {
|
||||
textSize = 14f
|
||||
textAlign = Paint.Align.LEFT
|
||||
typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
|
||||
color = Color.WHITE
|
||||
}
|
||||
|
||||
val tablePaint = Paint().apply {
|
||||
textSize = 12f
|
||||
textAlign = Paint.Align.LEFT
|
||||
color = secondaryColor
|
||||
}
|
||||
|
||||
// Draw headers and table
|
||||
drawPageHeaders(headerPaint, subHeaderPaint, selectedYear, selectedMonth)
|
||||
drawTableHeaders(tableHeaderPaint)
|
||||
|
||||
// Process each employee
|
||||
employees.forEach { employee ->
|
||||
if (currentY > pageHeight - 100) {
|
||||
finishCurrentPage()
|
||||
createNewPage()
|
||||
drawPageHeaders(headerPaint, subHeaderPaint, selectedYear, selectedMonth)
|
||||
drawTableHeaders(tableHeaderPaint)
|
||||
}
|
||||
|
||||
// Get attendance data
|
||||
val snapshot = Firebase.firestore
|
||||
.collection("attendance_${employee.Id}")
|
||||
.get()
|
||||
.await()
|
||||
|
||||
val monthStr = String.format("%02d", selectedMonth)
|
||||
val presentDates = snapshot.documents
|
||||
.filter { doc ->
|
||||
doc.id.matches(Regex("\\d{2}-$monthStr-$selectedYear"))
|
||||
}
|
||||
.map { doc ->
|
||||
doc.id.split("-")[0].toInt()
|
||||
}
|
||||
.sorted()
|
||||
|
||||
// Draw row background
|
||||
val rowPaint = Paint().apply {
|
||||
color = if ((employees.indexOf(employee) % 2) == 0) {
|
||||
backgroundColor
|
||||
} else {
|
||||
Color.WHITE
|
||||
}
|
||||
}
|
||||
|
||||
val rowHeight = 60f
|
||||
canvas.drawRect(startX, currentY, pageWidth - startX, currentY + rowHeight, rowPaint)
|
||||
|
||||
// Draw employee details
|
||||
val nameY = currentY + 25f
|
||||
canvas.drawText(employee.Name, startX + 10f, nameY, tablePaint)
|
||||
canvas.drawText(
|
||||
"${presentDates.size} days",
|
||||
startX + 190f,
|
||||
nameY,
|
||||
tablePaint
|
||||
)
|
||||
|
||||
// Format dates in groups of 5
|
||||
val datesText = presentDates.chunked(5).joinToString("\n") { chunk ->
|
||||
chunk.joinToString(", ") { day -> String.format("%02d", day) }
|
||||
}
|
||||
|
||||
// Draw dates in multiple lines if needed
|
||||
var dateY = nameY
|
||||
datesText.split("\n").forEach { line ->
|
||||
canvas.drawText(line, startX + 290f, dateY, tablePaint)
|
||||
dateY += 15f
|
||||
}
|
||||
|
||||
// Draw row borders
|
||||
val borderPaint = Paint().apply {
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = 0.5f
|
||||
color = borderColor
|
||||
}
|
||||
canvas.drawLine(
|
||||
startX, currentY + rowHeight,
|
||||
pageWidth - startX, currentY + rowHeight,
|
||||
borderPaint
|
||||
)
|
||||
|
||||
currentY += rowHeight
|
||||
}
|
||||
|
||||
// Finish and save document
|
||||
finishCurrentPage()
|
||||
|
||||
val fileName = "Attendance_Summary_${selectedYear}_${selectedMonth}.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 = 120f
|
||||
}
|
||||
|
||||
private fun finishCurrentPage() {
|
||||
document.finishPage(currentPage)
|
||||
}
|
||||
|
||||
private fun drawPageHeaders(
|
||||
headerPaint: Paint,
|
||||
subHeaderPaint: Paint,
|
||||
selectedYear: Int,
|
||||
selectedMonth: Int
|
||||
) {
|
||||
// Draw company logo background
|
||||
val logoBgPaint = Paint().apply {
|
||||
color = primaryColor
|
||||
alpha = 30
|
||||
}
|
||||
canvas.drawCircle(pageWidth / 2f, 45f, 100f, logoBgPaint)
|
||||
|
||||
// Draw company name
|
||||
canvas.drawText(companyName, pageWidth / 2f, 60f, headerPaint)
|
||||
|
||||
// Draw report title
|
||||
val monthName = SimpleDateFormat("MMMM", Locale.getDefault())
|
||||
.format(Calendar.getInstance().apply {
|
||||
set(Calendar.MONTH, selectedMonth - 1)
|
||||
}.time)
|
||||
|
||||
val reportTitle = "Monthly Attendance Summary - $monthName $selectedYear"
|
||||
canvas.drawText(reportTitle, pageWidth / 2f, 95f, subHeaderPaint)
|
||||
currentY = 140f
|
||||
}
|
||||
|
||||
private fun drawTableHeaders(headerPaint: Paint) {
|
||||
// Draw header background
|
||||
val headerBgPaint = Paint().apply {
|
||||
color = primaryColor
|
||||
}
|
||||
|
||||
val headerHeight = 40f
|
||||
canvas.drawRect(startX, currentY, pageWidth - startX, currentY + headerHeight, headerBgPaint)
|
||||
|
||||
// Draw column headers
|
||||
val textY = currentY + 25f
|
||||
canvas.drawText("Employee Name", startX + 10f, textY, headerPaint)
|
||||
canvas.drawText("Present Days", startX + 190f, textY, headerPaint)
|
||||
canvas.drawText("Dates Present", startX + 290f, textY, headerPaint)
|
||||
|
||||
currentY += headerHeight
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user