From 9c93677640489a7d24f6587f6d900e718b7b544c Mon Sep 17 00:00:00 2001 From: "MSI\\derek" Date: Mon, 13 May 2024 11:16:44 +0800 Subject: [PATCH] update --- .../modules/report/service/ReportService.kt | 219 +++++++++++++++++- .../modules/report/web/ReportController.kt | 36 ++- .../modules/report/web/model/ReportRequest.kt | 7 + ...08_Monthly Work Hours Analysis Report.xlsx | Bin 17782 -> 15765 bytes 4 files changed, 251 insertions(+), 11 deletions(-) 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 9570edac3b198d25138f805b3406a292d92c7ab3..a4e91ef0b86fc4a8bdbe4d88aaae90885152fbee 100644 GIT binary patch delta 7367 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 i{-aHhL7)eJTM@sS1mSoxkCC5x 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 o6oDLyGEo1oGf=>J_@Aq&qfAi