diff --git a/src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt b/src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt index c1ddc56..b95006c 100644 --- a/src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt +++ b/src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt @@ -1,11 +1,12 @@ package com.ffii.tsms.modules.report.service import com.ffii.tsms.modules.data.entity.Salary +import com.ffii.tsms.modules.data.entity.Staff import com.ffii.tsms.modules.project.entity.Invoice import com.ffii.tsms.modules.project.entity.Project import com.ffii.tsms.modules.timesheet.entity.Timesheet -import org.apache.poi.ss.usermodel.Sheet -import org.apache.poi.ss.usermodel.Workbook +import org.apache.poi.ss.usermodel.* +import org.apache.poi.ss.util.CellRangeAddress import org.apache.poi.xssf.usermodel.XSSFWorkbook import org.springframework.core.io.ClassPathResource import org.springframework.stereotype.Service @@ -13,13 +14,16 @@ import java.io.ByteArrayOutputStream import java.io.IOException import java.time.LocalDate import java.time.format.DateTimeFormatter +import java.util.* +data class DayInfo(val date: String, val weekday: String) @Service open class ReportService { private val DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy/MM/dd") private val FORMATTED_TODAY = LocalDate.now().format(DATE_FORMATTER) private val PROJECT_CASH_FLOW_REPORT = "templates/report/EX02_Project Cash Flow Report.xlsx" + private val MONTHLY_WORK_HOURS_ANALYSIS_REPORT = "templates/report/AR08_Monthly Work Hours Analysis Report.xlsx" private val SALART_LIST_TEMPLATE = "templates/report/Salary Template.xlsx" // ==============================|| GENERATE REPORT ||============================== // @@ -36,6 +40,19 @@ open class ReportService { return outputStream.toByteArray() } + @Throws(IOException::class) + fun generateStaffMonthlyWorkHourAnalysisReport(month: LocalDate, staff: Staff, timesheets: List, projectList: List): ByteArray { + // Generate the Excel report with query results + val workbook: Workbook = createStaffMonthlyWorkHourAnalysisReport(month, staff, timesheets, projectList, MONTHLY_WORK_HOURS_ANALYSIS_REPORT) + + // Write the workbook to a ByteArrayOutputStream + val outputStream: ByteArrayOutputStream = ByteArrayOutputStream() + workbook.write(outputStream) + workbook.close() + + return outputStream.toByteArray() + } + @Throws(IOException::class) fun exportSalaryList(salarys: List): ByteArray { // Generate the Excel report with query results @@ -108,11 +125,10 @@ open class ReportService { rowIndex = 10 val actualIncome = invoices.sumOf { invoice -> invoice.paidAmount!! } - val actualExpenditure = timesheets.sumOf { timesheet -> timesheet.staff!!.salary.hourlyRate.toDouble() * ((timesheet.normalConsumed ?: 0.0) + (timesheet.otConsumed ?: 0.0)) } sheet.getRow(rowIndex).apply { getCell(1).apply { // TODO: Replace by actual expenditure - setCellValue(actualExpenditure) +// setCellValue(actualIncome * 0.8) cellStyle.dataFormat = accountingStyle } @@ -137,6 +153,7 @@ open class ReportService { } // TODO: Add expenditure + // formula =IF(B17>0,D16-B17,D16+C17) rowIndex = 15 val combinedResults = (invoices.map { it.receiptDate } + timesheets.map { it.recordDate }).filterNotNull().sortedBy { it } @@ -171,6 +188,18 @@ open class ReportService { } cellStyle.dataFormat = accountingStyle } + getCell(3).apply { + val lastRow = rowIndex - 1 + if (lastRow == 15) { + cellFormula = "C{currentRow}".replace("{currentRow}", rowIndex.toString()) + } else { + cellFormula = "IF(B{currentRow}>0,D{lastRow}-B{currentRow},D{lastRow}+C{currentRow})".replace( + "{currentRow}", + rowIndex.toString() + ).replace("{lastRow}", lastRow.toString()) + } + cellStyle.dataFormat = accountingStyle + } getCell(4).apply { setCellValue(invoice.milestonePayment!!.description!!) @@ -215,6 +244,180 @@ open class ReportService { return workbook } + fun getColumnAlphabet(colIndex: Int): String { + val alphabet = StringBuilder() + + var index = colIndex + while (index >= 0) { + val remainder = index % 26 + val character = ('A'.code + remainder).toChar() + alphabet.insert(0, character) + index = (index / 26) - 1 + } + + return alphabet.toString() + } + + @Throws(IOException::class) + private fun createStaffMonthlyWorkHourAnalysisReport( + month: LocalDate, + staff: Staff, + timesheets: List, + projectList: List, + templatePath: String, + ): Workbook { +// val yearMonth = YearMonth.of(2022, 5) // May 2022 + val resource = ClassPathResource(templatePath) + val templateInputStream = resource.inputStream + val workbook: Workbook = XSSFWorkbook(templateInputStream) + + val accountingStyle = workbook.createDataFormat().getFormat("_(* #,##0.00_);_(* (#,##0.00);_(* \"-\"??_);_(@_)") + val monthStyle = workbook.createDataFormat().getFormat("mmm, yyyy") + + val daysOfMonth = (1..month.lengthOfMonth()).map { day -> + val date = month.withDayOfMonth(day) + val formattedDate = date.format(DateTimeFormatter.ofPattern("dd/MM/yyyy")) + val weekday = date.format(DateTimeFormatter.ofPattern("EEE", Locale.ENGLISH)) + DayInfo(formattedDate, weekday) + } + + val sheet: Sheet = workbook.getSheetAt(0) + + val boldFont = sheet.workbook.createFont() + boldFont.bold = true +// sheet.forceFormulaRecalculation = true; //Calculate formulas + + var rowIndex = 1 // Assuming the location is in (1,2), which is the report date field + var columnIndex = 1 + + var rowSize = 0 + var columnSize = 0 + +// tempCell = tempRow.createCell(columnIndex) + sheet.getRow(rowIndex).getCell(columnIndex).apply { + setCellValue(FORMATTED_TODAY) + } + println(sheet.getRow(1).getCell(2)) + + rowIndex = 2 + sheet.getRow(rowIndex).getCell(columnIndex).apply { + setCellValue(month) + cellStyle.dataFormat = monthStyle + } + + rowIndex = 3 + sheet.getRow(rowIndex).getCell(columnIndex).apply { + setCellValue(staff.name) + } + + rowIndex = 4 + sheet.getRow(rowIndex).getCell(columnIndex).apply { + setCellValue(staff.team.name) + } + + rowIndex = 5 + sheet.getRow(rowIndex).getCell(columnIndex).apply { + setCellValue(staff.grade.code) + } + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + rowIndex = 7 + daysOfMonth.forEach { dayInfo -> + rowIndex++ + rowSize++ + sheet.getRow(rowIndex).getCell(0).apply { + setCellValue(dayInfo.date) + cellStyle.setFont(boldFont) + } + sheet.getRow(rowIndex).getCell(1).apply { + setCellValue(dayInfo.weekday) + cellStyle.setFont(boldFont) + } + } + + rowIndex += 1 + sheet.getRow(rowIndex).getCell(0).apply { + setCellValue("Sub-total") + } + sheet.addMergedRegion(CellRangeAddress(rowIndex,rowIndex , 0, 1)) + // + rowIndex += 1 + sheet.getRow(rowIndex).getCell(0).apply { + setCellValue("Total Normal Hours [A]") + } + sheet.addMergedRegion(CellRangeAddress(rowIndex,rowIndex , 0, 1)) + // + rowIndex += 1 + sheet.getRow(rowIndex).getCell(0).apply { + setCellValue("Total Other Hours [B]") + sheet.addMergedRegion(CellRangeAddress(rowIndex,rowIndex , 0, 1)) + } + + rowIndex += 1 + sheet.getRow(rowIndex).getCell(0).apply { + setCellValue("Total Spent Manhours [A+B]") +// cellStyle.borderBottom = BorderStyle.DOUBLE + } + sheet.addMergedRegion(CellRangeAddress(rowIndex,rowIndex , 0, 1)) + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + rowIndex = 7 + columnIndex = 2 + projectList.forEachIndexed { index, title -> + sheet.getRow(7).getCell(columnIndex + index).apply { + setCellValue(title) + } + } + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + sheet.getRow(rowIndex).apply { + getCell(columnIndex).setCellValue("Leave Hours") + getCell(columnIndex).cellStyle.alignment = HorizontalAlignment.CENTER + + columnIndex += 1 + getCell(columnIndex).setCellValue("Daily Manhour Spent\n" + "(Excluding Leave Hours)") + getCell(columnIndex).cellStyle.alignment = HorizontalAlignment.CENTER +// columnSize = columnIndex + } + sheet.addMergedRegion(CellRangeAddress(6,6 , 2, columnIndex)) + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + for (i in 2 ..columnIndex) { + for (k in 0 until rowSize) { + sheet.getRow(8+k).getCell(i).apply { + setCellValue(" - ") + cellStyle.dataFormat = accountingStyle + } + } + } + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + if (timesheets.isNotEmpty()) { + projectList.forEachIndexed { i, _ -> + timesheets.forEach { timesheet -> + val dayInt = timesheet.recordDate!!.dayOfMonth + sheet.getRow(dayInt.plus(7)).getCell(columnIndex + i).apply { + setCellValue(timesheet.normalConsumed!!) + } + } + } + } + + rowIndex = 8 + println("rowSize is: $rowSize") + if (sheet.getRow(rowIndex - 1).getCell(2).stringCellValue == "Leave Hours") { + for (i in 0 until rowSize) { + val cell = sheet.getRow(rowIndex + i)?.getCell(columnIndex) ?: sheet.getRow(rowIndex + i)?.createCell(columnIndex) + cell?.cellFormula = "SUM(${getColumnAlphabet(2)}${rowIndex+1+i}:${getColumnAlphabet(columnIndex)}${rowIndex+1+i})" // should columnIndex - 2 +// cell?.setCellValue("testing") +// rowIndex++ + } +// for (i in 0 until columnSize) { +// val cell = sheet.getRow(rowIndex) +// } + } + + return workbook + } + + @Throws(IOException::class) private fun createSalaryList( salarys : List, @@ -235,15 +438,17 @@ open class ReportService { sheet.getRow(rowIndex++).apply { getCell(0).setCellValue(salary.salaryPoint.toDouble()) - when (index){ + when (index) { 0 -> getCell(1).setCellValue(salary.lowerLimit.toDouble()) - else -> getCell(1).cellFormula = "(C{previousRow}+1)".replace("{previousRow}", (rowIndex - 1).toString()) + else -> getCell(1).cellFormula = + "(C{previousRow}+1)".replace("{previousRow}", (rowIndex - 1).toString()) } getCell(2).cellFormula = "(B{currentRow}+D{currentRow})-1".replace("{currentRow}", rowIndex.toString()) // getCell(2).cellStyle.dataFormat = accountingStyle getCell(3).setCellValue(salary.increment.toDouble()) - getCell(4).cellFormula = "(((C{currentRow}+B{currentRow})/2)/20)/8".replace("{currentRow}", rowIndex.toString()) + getCell(4).cellFormula = + "(((C{currentRow}+B{currentRow})/2)/20)/8".replace("{currentRow}", rowIndex.toString()) // getCell(4).cellStyle.dataFormat = accountingStyle } } diff --git a/src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt b/src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt index 6ad099f..0edaee4 100644 --- a/src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt +++ b/src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt @@ -1,9 +1,12 @@ package com.ffii.tsms.modules.report.web +import com.ffii.tsms.modules.data.entity.StaffRepository +import com.ffii.tsms.modules.data.entity.projections.StaffSearchInfo import com.ffii.tsms.modules.project.entity.* import com.ffii.tsms.modules.report.service.ReportService import com.ffii.tsms.modules.project.service.InvoiceService import com.ffii.tsms.modules.report.web.model.ProjectCashFlowReportRequest +import com.ffii.tsms.modules.report.web.model.StaffMonthlyWorkHourAnalysisReportRequest import com.ffii.tsms.modules.timesheet.entity.TimesheetRepository import jakarta.validation.Valid import org.springframework.core.io.ByteArrayResource @@ -22,10 +25,15 @@ import java.time.LocalDate @RestController @RequestMapping("/reports") -class ReportController(private val invoiceRepository: InvoiceRepository, private val milestonePaymentRepository: MilestonePaymentRepository, private val excelReportService: ReportService, private val projectRepository: ProjectRepository, private val invoiceService: InvoiceService, - private val timesheetRepository: TimesheetRepository, - private val projectTaskRepository: ProjectTaskRepository -) { +class ReportController( + private val invoiceRepository: InvoiceRepository, + private val milestonePaymentRepository: MilestonePaymentRepository, + private val excelReportService: ReportService, + private val projectRepository: ProjectRepository, + private val timesheetRepository: TimesheetRepository, + private val projectTaskRepository: ProjectTaskRepository, + private val staffRepository: StaffRepository, + private val invoiceService: InvoiceService) { @PostMapping("/ProjectCashFlowReport") @Throws(ServletRequestBindingException::class, IOException::class) @@ -44,6 +52,26 @@ class ReportController(private val invoiceRepository: InvoiceRepository, private .body(ByteArrayResource(reportResult)) } + @PostMapping("/StaffMonthlyWorkHourAnalysisReport") + @Throws(ServletRequestBindingException::class, IOException::class) + fun StaffMonthlyWorkHourAnalysisReport(@RequestBody @Valid request: StaffMonthlyWorkHourAnalysisReportRequest): ResponseEntity { + val thisMonth = request.yearMonth.atDay(1) + val nextMonth = request.yearMonth.plusMonths(1).atDay(1) + + val staff = staffRepository.findById(request.id).orElseThrow() + val timesheets = timesheetRepository.findByStaffAndRecordDateBetweenOrderByRecordDate(staff, thisMonth, nextMonth) + + val projects = timesheetRepository.findDistinctProjectTaskByStaffAndRecordDateBetweenOrderByRecordDate(staff, thisMonth, nextMonth) + val projectList: List = projects.map { p -> "${p.projectTask!!.project!!.code}\n ${p.projectTask!!.project!!.name}" } + + val reportResult: ByteArray = excelReportService.generateStaffMonthlyWorkHourAnalysisReport(thisMonth, staff, timesheets, projectList) +// val mediaType: MediaType = MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + return ResponseEntity.ok() +// .contentType(mediaType) + .header("filename", "Monthly Work Hours Analysis Report - " + staff.name + " - " + LocalDate.now() + ".xlsx") + .body(ByteArrayResource(reportResult)) + } + @GetMapping("/test/{id}") fun test(@PathVariable id: Long): List { val project = projectRepository.findById(id).orElseThrow() diff --git a/src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt b/src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt index 99b6e61..bbb14da 100644 --- a/src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt +++ b/src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt @@ -1,5 +1,12 @@ package com.ffii.tsms.modules.report.web.model +import java.time.YearMonth + data class ProjectCashFlowReportRequest ( val projectId: Long +) + +data class StaffMonthlyWorkHourAnalysisReportRequest ( + val id: Long, + val yearMonth: YearMonth ) \ No newline at end of file diff --git a/src/main/resources/templates/report/AR08_Monthly Work Hours Analysis Report.xlsx b/src/main/resources/templates/report/AR08_Monthly Work Hours Analysis Report.xlsx index 9570eda..a4e91ef 100644 Binary files a/src/main/resources/templates/report/AR08_Monthly Work Hours Analysis Report.xlsx and b/src/main/resources/templates/report/AR08_Monthly Work Hours Analysis Report.xlsx differ