From 417f197fef454b73019769fde90ff537190e6d1d Mon Sep 17 00:00:00 2001 From: "MSI\\derek" Date: Tue, 21 May 2024 13:56:25 +0800 Subject: [PATCH] update work hour report --- .../projections/ProjectResourceReport.kt | 15 ++ .../modules/report/service/ReportService.kt | 205 +++++++++++++++--- .../modules/report/web/ReportController.kt | 58 +++-- .../modules/report/web/model/ReportRequest.kt | 5 + .../report/AR03_Resource Overconsumption.xlsx | Bin 14726 -> 13568 bytes 5 files changed, 243 insertions(+), 40 deletions(-) create mode 100644 src/main/java/com/ffii/tsms/modules/project/entity/projections/ProjectResourceReport.kt diff --git a/src/main/java/com/ffii/tsms/modules/project/entity/projections/ProjectResourceReport.kt b/src/main/java/com/ffii/tsms/modules/project/entity/projections/ProjectResourceReport.kt new file mode 100644 index 0000000..d949faa --- /dev/null +++ b/src/main/java/com/ffii/tsms/modules/project/entity/projections/ProjectResourceReport.kt @@ -0,0 +1,15 @@ +package com.ffii.tsms.modules.project.entity.projections + +data class ProjectResourceReport ( + val code: String, + val name: String, + val team: String, + val client: String, + val plannedBudget: Double, + val actualConsumedBudget: Double, + val plannedManhour: Double, + val actualConsumedManhour: Double, + val budgetConsumptionRate: Double, + val manhourConsumptionRate: Double, + val status: String +) \ No newline at end of file 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 666e109..863eb90 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 @@ -7,12 +7,7 @@ import com.ffii.tsms.modules.data.entity.Team import com.ffii.tsms.modules.data.entity.Customer import com.ffii.tsms.modules.project.entity.Invoice import com.ffii.tsms.modules.project.entity.Project -import com.ffii.tsms.modules.timesheet.entity.Leave import com.ffii.tsms.modules.timesheet.entity.Timesheet -import com.ffii.tsms.modules.timesheet.entity.projections.MonthlyLeave -import com.ffii.tsms.modules.timesheet.entity.projections.ProjectMonthlyHoursWithDate -import com.ffii.tsms.modules.timesheet.entity.projections.TimesheetHours -import com.ffii.tsms.modules.timesheet.web.models.LeaveEntry import org.apache.commons.logging.Log import org.apache.commons.logging.LogFactory import org.apache.poi.ss.usermodel.* @@ -25,13 +20,17 @@ import org.springframework.stereotype.Service import java.io.ByteArrayOutputStream import java.io.IOException import java.math.BigDecimal -import java.sql.Time import java.time.LocalDate import java.time.format.DateTimeFormatter import java.util.* data class DayInfo(val date: String?, val weekday: String?) + +//private operator fun Map.component2(): Any { +// +//} + @Service open class ReportService( private val jdbcDao: JdbcDao, @@ -45,6 +44,7 @@ open class ReportService( 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" private val LATE_START_REPORT = "templates/report/AR01_Late Start Report v01.xlsx" + private val RESOURCE_OVERCONSUMPTION_REPORT = "templates/report/AR03_Resource Overconsumption.xlsx" // ==============================|| GENERATE REPORT ||============================== // @@ -147,6 +147,27 @@ open class ReportService( return outputStream.toByteArray() } + @Throws(IOException::class) + fun generateProjectResourceOverconsumptionReport( + team: String, + customer: String, + result: List> + ): ByteArray { + // Generate the Excel report with query results + val workbook: Workbook = createProjectResourceOverconsumptionReport( + team, + customer, + result, + RESOURCE_OVERCONSUMPTION_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 @@ -737,10 +758,13 @@ open class ReportService( var projectList: List = listOf() println("----timesheets-----") println(timesheets) + + println("----leaves-----") + println(leaves) // result = timesheet record mapped var result: Map = mapOf() if (timesheets.isNotEmpty()) { - projectList = timesheets.map{ "${it["code"]}\n ${it["name"]}"}.toList() + projectList = timesheets.map{ "${it["code"]}\n ${it["name"]}"}.toList().distinct() result = timesheets.groupBy( { it["id"].toString() }, { mapOf( @@ -920,41 +944,40 @@ open class ReportService( columnSize++ } println(columnSize) - ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////// main data ////////////////////////////////////////////////////////////// if (timesheets.isNotEmpty()) { - projectList.forEach { _ -> + println("2") + projectList.forEachIndexed { index, _ -> for (i in 0 until rowSize) { - tempCell = sheet.getRow(8 + i).createCell(columnIndex) + tempCell = sheet.getRow(8 + i).createCell(columnIndex + index) tempCell.setCellValue(0.0) tempCell.cellStyle.dataFormat = accountingStyle } - result.forEach{(id, list) -> - for (i in 0 until id.toInt()) { - val temp: List> = list as List> - temp.forEachIndexed { i, _ -> - dayInt = temp[i]["date"].toString().toInt() - tempCell = sheet.getRow(dayInt.plus(7)).createCell(columnIndex) - tempCell.setCellValue(temp[i]["normalConsumed"] as Double) - } + } + result.forEach{ (id, list) -> + val temp: List> = list as List> + temp.forEachIndexed { index, _ -> + dayInt = temp[index]["date"].toString().toInt() + tempCell = sheet.getRow(dayInt.plus(7)).createCell(columnIndex) + tempCell.setCellValue(temp[index]["normalConsumed"] as Double) } - } columnIndex++ } } - // leave hours data + /////////////////////////////////////////////////////////////// leave hours data ////////////////////////////////////////////////////////////// + for (i in 0 until rowSize) { + tempCell = sheet.getRow(8 + i).createCell(columnIndex) + tempCell.setCellValue(0.0) + tempCell.cellStyle.dataFormat = accountingStyle + } if (leaves.isNotEmpty()) { leaves.forEach { leave -> - for (i in 0 until rowSize) { - tempCell = sheet.getRow(8 + i).createCell(columnIndex) - tempCell.setCellValue(0.0) - tempCell.cellStyle.dataFormat = accountingStyle - } dayInt = leave["recordDate"].toString().toInt() tempCell = sheet.getRow(dayInt.plus(7)).createCell(columnIndex) tempCell.setCellValue(leave["leaveHours"] as Double) } } - ///////////////////////////////////////////////////////// Leave Hours //////////////////////////////////////////////////////////////////// + ///////////////////////////////////////////////////////// Leave Hours title //////////////////////////////////////////////////////////////////// tempCell = sheet.getRow(rowIndex).createCell(columnIndex) tempCell.setCellValue("Leave Hours") tempCell.cellStyle = boldStyle @@ -1031,6 +1054,62 @@ open class ReportService( return workbook } + + private fun createProjectResourceOverconsumptionReport( + team: String, + customer: String, + result: List>, + templatePath: String + ):Workbook { + val resource = ClassPathResource(templatePath) + val templateInputStream = resource.inputStream + val workbook: Workbook = XSSFWorkbook(templateInputStream) + + val accountingStyle = workbook.createDataFormat().getFormat("_(* #,##0.00_);_(* (#,##0.00);_(* \"-\"??_);_(@_)") + + val sheet: Sheet = workbook.getSheetAt(0) + + val alignCenterStyle = workbook.createCellStyle() + alignCenterStyle.alignment = HorizontalAlignment.CENTER // Set the alignment to center + + var rowIndex = 1 // Assuming the location is in (1,2), which is the report date field + var columnIndex = 2 + var tempRow = sheet.getRow(rowIndex) + var tempCell = tempRow.getCell(columnIndex) + tempCell.setCellValue(FORMATTED_TODAY) + + rowIndex = 2 + tempCell = sheet.getRow(rowIndex).getCell(columnIndex) + tempCell.setCellValue(team) + + rowIndex = 3 + tempCell = sheet.getRow(rowIndex).getCell(columnIndex) + tempCell.setCellValue(customer) + + rowIndex = 6 + columnIndex = 0 + result.forEachIndexed { index, obj -> + tempCell = sheet.getRow(rowIndex).getCell(columnIndex) + tempCell.setCellValue((index + 1).toDouble()) + val keys = obj.keys.toList() + keys.forEachIndexed { keyIndex, key -> + tempCell = sheet.getRow(rowIndex).getCell(columnIndex + keyIndex + 1) + when (obj[key]) { + is String -> tempCell.setCellValue(obj[key] as String) + is Double -> tempCell.setCellValue(obj[key] as Double) +// else -> tempCell.setCellValue(obj[key] as { + +// }) + } + } + rowIndex++ + } +// tempCell = sheet.getRow(rowIndex).getCell(columnIndex) +// tempCell.setCellValue() + + return workbook + } + //createLateStartReport private fun createLateStartReport( team: Team, @@ -1177,4 +1256,78 @@ open class ReportService( return jdbcDao.queryForList(sql.toString(), args) } + open fun getProjectResourceOverconsumptionReport(args: Map): List> { + val sql = StringBuilder("WITH teamNormalConsumed AS (" + + " SELECT " + + " s.teamId, " + + " pt.project_id, " + + " SUM(tns.totalConsumed) AS totalConsumed " + + " FROM ( " + + " SELECT " + + " t.staffId, " + + " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) as totalConsumed, " + + " t.projectTaskId AS taskId " + + " FROM timesheet t " + + " LEFT JOIN staff s ON t.staffId = s.id " + + " GROUP BY t.staffId, t.projectTaskId " + + " ) AS tns " + + " INNER JOIN project_task pt ON tns.taskId = pt.id " + + " JOIN staff s ON tns.staffId = s.id " + + " JOIN team t ON s.teamId = t.id " + + " GROUP BY teamId, project_id " + + " ) " + + " SELECT " + + " p.code, " + + " -- p.status, " + + " p.name, " + + " t.code as team, " + + " c.code as client, " + + " p.expectedTotalFee as plannedBudget, " + + " COALESCE((tns.totalConsumed * sa.hourlyRate), 0) as actualConsumedBudget, " + + " COALESCE(p.totalManhour, 0) as plannedManhour, " + + " COALESCE(tns.totalConsumed, 0) as actualConsumedManhour, " + + " (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) as budgetConsumptionRate, " + + " (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) as manhourConsumptionRate, " + + " CASE " + + " when (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= 0.9 and (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) <= 1 " + + " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= 0.9 and (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) <= 1 " + + " then 'Potential Overconsumption' " + + " when (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= 1 " + + " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= 1 " + + " then 'Overconsumption' " + + " else 'Within Budget' " + + " END as status " + + " FROM project p " + + " LEFT JOIN team t ON p.teamLead = t.teamLead " + + " LEFT JOIN staff s ON p.teamLead = s.id " + + " LEFT JOIN salary sa ON s.salaryId = sa.salaryPoint " + + " LEFT JOIN customer c ON p.customerId = c.id " + + " left join teamNormalConsumed tns on tns.project_id = p.id " + + " WHERE p.deleted = false " + + " and p.status = 'On-going' " + ) + if (args != null) { + var statusFilter: String = "" + if (args.containsKey("teamId")) + sql.append("and t.id = :teamId") + if (args.containsKey("custId")) + sql.append("and c.id = :custId") + if (args.containsKey("status")) + statusFilter = when (args.get("status")) { + "Potential Overconsumption" -> " ( and " + + "((COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= 0.9 " + + "and (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) <= 1 " + + "or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= 0.9 " + + "and (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) <= 1)" + + " ) " + "Overconsumption" -> " ( and " + + "((COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= 1 " + + "or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= 1)" + + " ) " + else -> "" + } + sql.append(statusFilter) + } + return jdbcDao.queryForList(sql.toString(), args) + } } 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 11f16de..b4a4bee 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,33 +1,34 @@ package com.ffii.tsms.modules.report.web +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.KotlinModule +import com.ffii.tsms.modules.data.entity.Customer import com.ffii.tsms.modules.data.entity.StaffRepository //import com.ffii.tsms.modules.data.entity.projections.FinancialStatusReportInfo import com.ffii.tsms.modules.data.entity.projections.StaffSearchInfo +import com.ffii.tsms.modules.data.entity.Team +import com.ffii.tsms.modules.data.service.CustomerService +import com.ffii.tsms.modules.data.service.TeamService import com.ffii.tsms.modules.project.entity.* -import com.ffii.tsms.modules.report.service.ReportService +import com.ffii.tsms.modules.project.entity.projections.ProjectResourceReport import com.ffii.tsms.modules.project.service.InvoiceService +import com.ffii.tsms.modules.report.service.ReportService import com.ffii.tsms.modules.report.web.model.FinancialStatusReportRequest import com.ffii.tsms.modules.report.web.model.ProjectCashFlowReportRequest +import com.ffii.tsms.modules.report.web.model.ProjectResourceOverconsumptionReport import com.ffii.tsms.modules.report.web.model.StaffMonthlyWorkHourAnalysisReportRequest import com.ffii.tsms.modules.report.web.model.LateStartReportRequest import com.ffii.tsms.modules.project.entity.ProjectRepository import com.ffii.tsms.modules.timesheet.entity.LeaveRepository import com.ffii.tsms.modules.timesheet.entity.TimesheetRepository -import com.ffii.tsms.modules.timesheet.entity.projections.ProjectMonthlyHoursWithDate import jakarta.validation.Valid import org.springframework.core.io.ByteArrayResource import org.springframework.core.io.Resource -import org.springframework.http.ResponseEntity -import org.springframework.web.bind.ServletRequestBindingException -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PathVariable -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RequestParam -import org.springframework.web.bind.annotation.RestController import org.springframework.http.HttpHeaders import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.ServletRequestBindingException +import org.springframework.web.bind.annotation.* import java.io.IOException import java.time.LocalDate import java.net.URLEncoder @@ -36,7 +37,6 @@ import org.springframework.stereotype.Controller import com.ffii.tsms.modules.data.entity.TeamRepository import com.ffii.tsms.modules.data.entity.CustomerRepository import org.springframework.data.jpa.repository.JpaRepository -import com.ffii.tsms.modules.data.entity.Customer import com.ffii.tsms.modules.project.entity.Project @RestController @@ -52,6 +52,8 @@ class ReportController( private val customerRepository: CustomerRepository, private val staffRepository: StaffRepository, private val leaveRepository: LeaveRepository, + private val teamService: TeamService, + private val customerService: CustomerService, private val invoiceService: InvoiceService) { @PostMapping("/fetchProjectsFinancialStatusReport") @@ -90,13 +92,16 @@ class ReportController( val nextMonth = request.yearMonth.plusMonths(1).atDay(1) val staff = staffRepository.findById(request.id).orElseThrow() + println(thisMonth) + println(nextMonth) val args: Map = mutableMapOf( "staffId" to request.id, "startDate" to thisMonth, "endDate" to nextMonth, ) - val timesheets= excelReportService.getTimesheet(args) - val leaves= excelReportService.getLeaves(args) + val timesheets = excelReportService.getTimesheet(args) + val leaves = excelReportService.getLeaves(args) + println(leaves) val reportResult: ByteArray = excelReportService.generateStaffMonthlyWorkHourAnalysisReport(thisMonth, staff, timesheets, leaves) // val mediaType: MediaType = MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") @@ -105,6 +110,31 @@ class ReportController( .header("filename", "Monthly Work Hours Analysis Report - " + staff.name + " - " + LocalDate.now() + ".xlsx") .body(ByteArrayResource(reportResult)) } + private val mapper = ObjectMapper().registerModule(KotlinModule()) + + @PostMapping("/ProjectResourceOverconsumptionReport") + @Throws(ServletRequestBindingException::class, IOException::class) + fun ProjectResourceOverconsumptionReport(@RequestBody @Valid request: ProjectResourceOverconsumptionReport): ResponseEntity { + +// val staff = staffRepository.findById(request.id).orElseThrow() + val args: Map = mutableMapOf( + "teamId" to request.teamId, + "custId" to request.custId, + "status" to request.status + ) + val team: String = teamService.find(request.teamId).orElseThrow().name + val customer: String = customerService.find(request.custId).orElseThrow().name + val result: List> = excelReportService.getProjectResourceOverconsumptionReport(args); +// val obj: ProjectResourceReport = mapper.readValue(mapper.writeValueAsBytes(result)) + + + val reportResult: ByteArray = excelReportService.generateProjectResourceOverconsumptionReport(team, customer, result) +// val mediaType: MediaType = MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + return ResponseEntity.ok() +// .contentType(mediaType) + .header("filename", "Project Resource Overconsumption Report - " + " - " + LocalDate.now() + ".xlsx") + .body(ByteArrayResource(reportResult)) + } @GetMapping("/test/{id}") fun test(@PathVariable id: Long): List { 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 8a85bc5..54954c7 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 @@ -23,4 +23,9 @@ data class LateStartReportRequest ( val remainedDateTo: LocalDate, val customer: String, val reportDate: LocalDate +) +data class ProjectResourceOverconsumptionReport ( + val teamId: Long, + val custId: Long, + val status: String, ) \ No newline at end of file diff --git a/src/main/resources/templates/report/AR03_Resource Overconsumption.xlsx b/src/main/resources/templates/report/AR03_Resource Overconsumption.xlsx index dc4f048174101be1f4a7c801e9f7e8f533b5c609..8ef3799c357d1ba14b447f1c01177aeeefcdfb30 100644 GIT binary patch delta 5893 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 SLVr