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 dc4f048..8ef3799 100644 Binary files a/src/main/resources/templates/report/AR03_Resource Overconsumption.xlsx and b/src/main/resources/templates/report/AR03_Resource Overconsumption.xlsx differ