From eba4783a88f70353fb3cece64f4a0ac13827b3cc Mon Sep 17 00:00:00 2001 From: chandrashekhar reddy Date: Tue, 7 Jan 2025 15:14:13 +0530 Subject: [PATCH] generate pdf code added --- app/build.gradle | 4 +- .../sminnovations/smiadmin/ui/CSVGenerator.kt | 177 -------------- .../sminnovations/smiadmin/ui/ExportTask.kt | 82 +++++++ .../sminnovations/smiadmin/ui/HomeFragment.kt | 73 +++--- .../sminnovations/smiadmin/ui/PDFGenerator.kt | 222 ++++++++++++++++++ 5 files changed, 346 insertions(+), 212 deletions(-) delete mode 100644 app/src/main/java/in/sminnovations/smiadmin/ui/CSVGenerator.kt create mode 100644 app/src/main/java/in/sminnovations/smiadmin/ui/ExportTask.kt create mode 100644 app/src/main/java/in/sminnovations/smiadmin/ui/PDFGenerator.kt diff --git a/app/build.gradle b/app/build.gradle index 103b525..15ab599 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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" } diff --git a/app/src/main/java/in/sminnovations/smiadmin/ui/CSVGenerator.kt b/app/src/main/java/in/sminnovations/smiadmin/ui/CSVGenerator.kt deleted file mode 100644 index f550d34..0000000 --- a/app/src/main/java/in/sminnovations/smiadmin/ui/CSVGenerator.kt +++ /dev/null @@ -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, - 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() - val attendanceData = mutableMapOf>() - - // 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() - 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() - val attendanceData = mutableMapOf>() - - 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() - 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 - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/in/sminnovations/smiadmin/ui/ExportTask.kt b/app/src/main/java/in/sminnovations/smiadmin/ui/ExportTask.kt new file mode 100644 index 0000000..797d59c --- /dev/null +++ b/app/src/main/java/in/sminnovations/smiadmin/ui/ExportTask.kt @@ -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() + } +} diff --git a/app/src/main/java/in/sminnovations/smiadmin/ui/HomeFragment.kt b/app/src/main/java/in/sminnovations/smiadmin/ui/HomeFragment.kt index 56c5d37..1e8adbe 100644 --- a/app/src/main/java/in/sminnovations/smiadmin/ui/HomeFragment.kt +++ b/app/src/main/java/in/sminnovations/smiadmin/ui/HomeFragment.kt @@ -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() + } } \ No newline at end of file diff --git a/app/src/main/java/in/sminnovations/smiadmin/ui/PDFGenerator.kt b/app/src/main/java/in/sminnovations/smiadmin/ui/PDFGenerator.kt new file mode 100644 index 0000000..23175b1 --- /dev/null +++ b/app/src/main/java/in/sminnovations/smiadmin/ui/PDFGenerator.kt @@ -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, + 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 + } +} \ No newline at end of file