updated PDFGenerator daily.kt
This commit is contained in:
@@ -0,0 +1,500 @@
|
|||||||
|
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 = 40f
|
||||||
|
private val contentWidth = pageWidth - (2 * margin)
|
||||||
|
|
||||||
|
// Professional color scheme
|
||||||
|
private val primaryColor = Color.rgb(41, 128, 185) // Professional blue
|
||||||
|
private val secondaryColor = Color.rgb(44, 62, 80) // Dark blue
|
||||||
|
private val accentColor = Color.rgb(230, 126, 34) // Orange for highlights
|
||||||
|
private val backgroundColor = Color.rgb(236, 240, 241) // Light gray
|
||||||
|
private val borderColor = Color.rgb(189, 195, 199) // Medium gray
|
||||||
|
private val textColor = Color.rgb(52, 73, 94) // Dark gray for text
|
||||||
|
|
||||||
|
private var pageNumber = 1
|
||||||
|
|
||||||
|
// 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 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"
|
||||||
|
|
||||||
|
// 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 Times", xPos, currentY + 30f, headerPaint)
|
||||||
|
|
||||||
|
xPos += 150f
|
||||||
|
canvas.drawText("Check-out Times", xPos, currentY + 30f, headerPaint)
|
||||||
|
|
||||||
|
xPos += 110f
|
||||||
|
canvas.drawText("Total 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(231, 76, 60) // Red 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(
|
||||||
|
String.format("%.2f hrs", 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 hrs",
|
||||||
|
margin + 505f,
|
||||||
|
currentY + 25f,
|
||||||
|
highlightPaint
|
||||||
|
)
|
||||||
|
|
||||||
|
currentY += rowHeight + 10f
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun calculateTotalHours(checkInTimes: List<String>, checkOutTimes: List<String>): Float {
|
||||||
|
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 minutes to hours
|
||||||
|
return totalMinutes / 60
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawFooter(paint: Paint) {
|
||||||
|
paint.textAlign = Paint.Align.CENTER
|
||||||
|
paint.textSize = 10f
|
||||||
|
|
||||||
|
val currentDate = SimpleDateFormat("dd MMMM yyyy HH:mm", Locale.getDefault())
|
||||||
|
.format(Calendar.getInstance().time)
|
||||||
|
|
||||||
|
canvas.drawText(
|
||||||
|
"Generated on: $currentDate | Page $pageNumber",
|
||||||
|
pageWidth/2,
|
||||||
|
pageHeight - margin/2,
|
||||||
|
paint
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user