|
|
@@ -33,10 +33,8 @@ import java.util.* |
|
|
|
import java.awt.Color |
|
|
|
import java.lang.IllegalArgumentException |
|
|
|
import java.math.RoundingMode |
|
|
|
import java.time.Year |
|
|
|
import javax.swing.plaf.synth.Region |
|
|
|
import java.time.DayOfWeek |
|
|
|
import kotlin.collections.ArrayList |
|
|
|
import kotlin.collections.HashMap |
|
|
|
|
|
|
|
|
|
|
|
data class DayInfo(val date: String?, val weekday: String?) |
|
|
@@ -76,6 +74,7 @@ open class ReportService( |
|
|
|
private val COMPLETION_PROJECT = "templates/report/AR05_Project Completion Report.xlsx" |
|
|
|
private val CROSS_TEAM_CHARGE_REPORT = "templates/report/Cross Team Charge Report.xlsx" |
|
|
|
private val PROJECT_MANHOUR_SUMMARY = "templates/report/Project Manhour Summary.xlsx" |
|
|
|
private val PROJECT_MONTHLY_REPORT = "templates/report/AR09_Project Daily Work Hours Analysis Report.xlsx" |
|
|
|
|
|
|
|
private fun cellBorderArgs(top: Int, bottom: Int, left: Int, right: Int): MutableMap<String, Any> { |
|
|
|
var cellBorderArgs = mutableMapOf<String, Any>() |
|
|
@@ -5425,4 +5424,232 @@ open class ReportService( |
|
|
|
ProjectSummary(staffData, projectCumulativeExpenditure) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
data class DateInfo( |
|
|
|
val date: LocalDate?, |
|
|
|
val isHoliday: Boolean |
|
|
|
) |
|
|
|
|
|
|
|
fun convertStringToLocalDate(dateStrings: List<String>, pattern: String = "dd/MM/yyyy"): List<LocalDate> { |
|
|
|
// Define the formatter for the date string (dd/MM/yyyy format) |
|
|
|
val formatter = DateTimeFormatter.ofPattern(pattern) |
|
|
|
|
|
|
|
// Convert each string in the list to LocalDate |
|
|
|
return dateStrings.mapNotNull { dateString -> |
|
|
|
try { |
|
|
|
LocalDate.parse(dateString, formatter) |
|
|
|
} catch (e: DateTimeParseException) { |
|
|
|
println("Invalid date format: $dateString") // Log the invalid date |
|
|
|
null // Exclude invalid dates from the result |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
// Function to generate all dates in a month |
|
|
|
fun generateDatesForMonthWithHolidays(yearMonth: YearMonth, holidays: List<LocalDate>): List<DateInfo> { |
|
|
|
|
|
|
|
return (1..yearMonth.lengthOfMonth()).map { day -> |
|
|
|
val date = yearMonth.atDay(day) |
|
|
|
val isHoliday = date in holidays || date.dayOfWeek == DayOfWeek.SATURDAY || date.dayOfWeek == DayOfWeek.SUNDAY |
|
|
|
DateInfo(date, isHoliday) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
fun columnIndexToLetter(columnIndex: Int): String { |
|
|
|
val letters = StringBuilder() |
|
|
|
var index = columnIndex |
|
|
|
|
|
|
|
// Convert to Excel column letters (1-based index) |
|
|
|
while (index > 0) { |
|
|
|
val letterIndex = (index - 1) % 26 |
|
|
|
letters.insert(0, ('A' + letterIndex).toChar()) |
|
|
|
index = (index - letterIndex) / 26 |
|
|
|
} |
|
|
|
|
|
|
|
return letters.toString() |
|
|
|
} |
|
|
|
|
|
|
|
@Throws(IOException::class) |
|
|
|
private fun createProjectMonthlyWorkbook( |
|
|
|
yearMonth: YearMonth, |
|
|
|
project: Project, |
|
|
|
timesheets: List<Timesheet>, |
|
|
|
monthList: List<DateInfo>, |
|
|
|
templatePath: String, |
|
|
|
): Workbook { |
|
|
|
val resource = ClassPathResource(templatePath) |
|
|
|
val templateInputStream = resource.inputStream |
|
|
|
val workbook: Workbook = XSSFWorkbook(templateInputStream) |
|
|
|
|
|
|
|
val monthStyle = workbook.createDataFormat().getFormat("MMM YYYY") |
|
|
|
val formatter = DateTimeFormatter.ofPattern("MMM yyyy") |
|
|
|
|
|
|
|
// Style Related |
|
|
|
val redFont = workbook.createFont().apply { |
|
|
|
color = IndexedColors.RED.index |
|
|
|
fontHeightInPoints = 12 |
|
|
|
} |
|
|
|
|
|
|
|
val blackFont = workbook.createFont().apply { |
|
|
|
color = IndexedColors.BLACK.index |
|
|
|
fontHeightInPoints = 12 |
|
|
|
} |
|
|
|
|
|
|
|
val redFontStyle = workbook.createCellStyle().apply { |
|
|
|
setFont(redFont) |
|
|
|
borderTop = BorderStyle.THIN |
|
|
|
borderBottom = BorderStyle.THIN |
|
|
|
borderLeft = BorderStyle.THIN |
|
|
|
borderRight = BorderStyle.THIN |
|
|
|
} |
|
|
|
|
|
|
|
val blackFontStyle = workbook.createCellStyle().apply { |
|
|
|
setFont(blackFont) |
|
|
|
borderTop = BorderStyle.THIN |
|
|
|
borderBottom = BorderStyle.THIN |
|
|
|
borderLeft = BorderStyle.THIN |
|
|
|
borderRight = BorderStyle.THIN |
|
|
|
} |
|
|
|
|
|
|
|
val yellowBackgroundStyle = workbook.createCellStyle().apply { |
|
|
|
setFont(blackFont) |
|
|
|
borderTop = BorderStyle.THIN |
|
|
|
borderBottom = BorderStyle.THIN |
|
|
|
borderLeft = BorderStyle.THIN |
|
|
|
borderRight = BorderStyle.THIN |
|
|
|
|
|
|
|
fillForegroundColor = IndexedColors.YELLOW.index |
|
|
|
fillPattern = FillPatternType.SOLID_FOREGROUND |
|
|
|
} |
|
|
|
|
|
|
|
var sheet: Sheet = workbook.getSheetAt(0) |
|
|
|
|
|
|
|
val staffs : List<Staff> = timesheets.mapNotNull { it.staff }.distinct() |
|
|
|
val timesheetRecordsByStaff = timesheets.groupBy ({it.staff}, {it}) |
|
|
|
.mapValues { (_, records) -> |
|
|
|
records.groupBy( {it.recordDate}, {(it.normalConsumed ?: 0.0) + (it.otConsumed ?: 0.0)}) |
|
|
|
.mapValues { it.value.sum() } |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
var rowIndex = 1 // Assuming the location is in (1,2), which is the report date field |
|
|
|
var columnIndex = 1 |
|
|
|
|
|
|
|
sheet.getRow(rowIndex).createCell(columnIndex+1).apply { |
|
|
|
setCellValue(FORMATTED_TODAY) |
|
|
|
} |
|
|
|
|
|
|
|
val startRowIndex = 6 |
|
|
|
val startColumnIndex = 2 |
|
|
|
|
|
|
|
rowIndex = 2 |
|
|
|
sheet.getRow(rowIndex).getCell(columnIndex+1).apply { |
|
|
|
setCellValue(yearMonth.format(formatter)) |
|
|
|
cellStyle.dataFormat = monthStyle |
|
|
|
} |
|
|
|
|
|
|
|
rowIndex = 3 |
|
|
|
sheet.getRow(rowIndex).getCell(columnIndex+1).apply { |
|
|
|
setCellValue( "${project.code} - ${project.name}" ) |
|
|
|
} |
|
|
|
|
|
|
|
// date header row |
|
|
|
rowIndex = 5 |
|
|
|
columnIndex = 2 |
|
|
|
for (date in monthList){ |
|
|
|
val row = sheet.getRow(rowIndex) |
|
|
|
val dateCell = row.createCell(columnIndex) |
|
|
|
dateCell.apply { |
|
|
|
setCellValue(date.date?.format(DateTimeFormatter.ofPattern("dd"))) |
|
|
|
cellStyle = blackFontStyle |
|
|
|
if(date.isHoliday){ |
|
|
|
cellStyle.apply { cellStyle = redFontStyle } |
|
|
|
} |
|
|
|
} |
|
|
|
sheet.autoSizeColumn(columnIndex) |
|
|
|
columnIndex++ |
|
|
|
} |
|
|
|
|
|
|
|
// |
|
|
|
rowIndex = 6 |
|
|
|
timesheetRecordsByStaff.forEach{(staff, dates) -> |
|
|
|
val row = sheet.getRow(rowIndex) |
|
|
|
val staffIdCell = row.createCell(0) |
|
|
|
val staffNameCell = row.createCell(1) |
|
|
|
staffIdCell.apply { |
|
|
|
setCellValue(staff?.staffId ?: "") |
|
|
|
cellStyle =blackFontStyle |
|
|
|
} |
|
|
|
staffNameCell.apply { |
|
|
|
setCellValue(staff?.name ?: "") |
|
|
|
cellStyle =blackFontStyle |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
var recordDateCol = 2 |
|
|
|
monthList.forEachIndexed { index, dateInfo -> |
|
|
|
val recordDateByStaffCell = row.createCell(2+index) |
|
|
|
recordDateByStaffCell.cellStyle = blackFontStyle |
|
|
|
if(dates.keys.contains(dateInfo.date)){ |
|
|
|
recordDateByStaffCell.setCellValue(dates.getValue(dateInfo.date)) |
|
|
|
}else{ |
|
|
|
recordDateByStaffCell.setCellValue("-") |
|
|
|
} |
|
|
|
recordDateCol++ |
|
|
|
} |
|
|
|
rowIndex++ |
|
|
|
} |
|
|
|
|
|
|
|
val lastRowIndex = rowIndex |
|
|
|
val totalRow = sheet.getRow(lastRowIndex) |
|
|
|
|
|
|
|
// calculate the sum of work hours spent per day |
|
|
|
val numberOfColumnns = monthList.size |
|
|
|
val lastColumnIndex = startColumnIndex + numberOfColumnns |
|
|
|
for (colIndex in startColumnIndex until lastColumnIndex){ |
|
|
|
val columnLetter = columnIndexToLetter(colIndex + 1) |
|
|
|
val totalSumFormula = "SUM($columnLetter${6 + 1}:$columnLetter${lastRowIndex})" // |
|
|
|
val totalSumCell = totalRow.createCell(colIndex) |
|
|
|
totalSumCell.apply { |
|
|
|
cellFormula = totalSumFormula |
|
|
|
cellStyle = yellowBackgroundStyle |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// calculate the sum of work hour per staff |
|
|
|
val startColumnLetter = columnIndexToLetter(startColumnIndex) |
|
|
|
val lastColumnLetter = columnIndexToLetter(lastColumnIndex) |
|
|
|
for (index in startRowIndex .. lastRowIndex){ |
|
|
|
val staffTotalSumFormula = "SUM($startColumnLetter${index+1}:$lastColumnLetter${index+1})" // excel start from 1, index start from 0 |
|
|
|
val staffTotalRow = sheet.getRow(index) |
|
|
|
val staffTotalRowCell = staffTotalRow.createCell(lastColumnIndex) |
|
|
|
staffTotalRowCell.apply { |
|
|
|
cellFormula = staffTotalSumFormula |
|
|
|
cellStyle = yellowBackgroundStyle |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return workbook |
|
|
|
} |
|
|
|
|
|
|
|
fun genProjectMonthlyReport( |
|
|
|
yearMonth: YearMonth, |
|
|
|
projectId: Long, |
|
|
|
holidays: List<String>, |
|
|
|
): ByteArray{ |
|
|
|
|
|
|
|
val project = projectRepository.findById(projectId).get(); |
|
|
|
|
|
|
|
val year = yearMonth.year |
|
|
|
val month = yearMonth.monthValue |
|
|
|
val timesheetRecords : List<Timesheet> = timesheetRepository.findByYearAndMonthAndProjectId(year, month, projectId) |
|
|
|
val monthList = generateDatesForMonthWithHolidays(yearMonth, convertStringToLocalDate(holidays)) |
|
|
|
|
|
|
|
val workbook: Workbook = createProjectMonthlyWorkbook(yearMonth, project, timesheetRecords, monthList, PROJECT_MONTHLY_REPORT) |
|
|
|
|
|
|
|
val outputStream: ByteArrayOutputStream = ByteArrayOutputStream() |
|
|
|
workbook.write(outputStream) |
|
|
|
workbook.close() |
|
|
|
|
|
|
|
return outputStream.toByteArray() |
|
|
|
} |
|
|
|
} |