From 6b811b14df8e4ebb139a3aa1132b49716cd2b8ee Mon Sep 17 00:00:00 2001 From: "MSI\\derek" Date: Fri, 22 Nov 2024 14:30:40 +0800 Subject: [PATCH] add: project manhour report update: order project lists --- .../modules/data/service/DashboardService.kt | 37 ++-- .../entity/StaffAllocationRepository.kt | 2 +- .../project/service/ProjectsService.kt | 24 --- .../modules/report/service/ReportService.kt | 182 +++++++++++++++++- .../modules/report/web/ReportController.kt | 16 ++ .../modules/report/web/model/ReportRequest.kt | 5 + .../report/Project Manhour Summary.xlsx | Bin 0 -> 9498 bytes 7 files changed, 222 insertions(+), 44 deletions(-) create mode 100644 src/main/resources/templates/report/Project Manhour Summary.xlsx diff --git a/src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt b/src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt index 809a666..60714a4 100644 --- a/src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt +++ b/src/main/java/com/ffii/tsms/modules/data/service/DashboardService.kt @@ -395,7 +395,7 @@ open class DashboardService( if (viewDashboardAuthority() == "self") { val teamId = staffsService.currentStaff()?.team?.id - if (teamId != null) { + if (teamId != null && teamId > 0) { sql.append(" and t.id = $teamId") } } @@ -406,18 +406,18 @@ open class DashboardService( + " from teamProject tp" ) - if (viewDashboardAuthority() != "self") { - sql.append(" union all" - + " select" - + " null as id," - + " null as teamId," - + " null as teamLeadId," - + " 'All' as teamCode," - + " null as teamName," - + " sum(tp.projectNo) as projectNo" - + " from teamProject tp" - + " order by id") - } +// if (viewDashboardAuthority() != "self") { +// sql.append(" union all" +// + " select" +// + " null as id," +// + " null as teamId," +// + " null as teamLeadId," +// + " 'All' as teamCode," +// + " null as teamName," +// + " sum(tp.projectNo) as projectNo" +// + " from teamProject tp" +// + " order by teamCode") +// } return jdbcDao.queryForList(sql.toString(), args) } @@ -487,8 +487,8 @@ open class DashboardService( + " group by p.id, p.code, p.name, te.code, s.name, p.totalManhour, milestonePayment.comingPaymentMilestone, taskGroup.expectedStage" ) - if (args["tableSorting"] == "ProjectName") { - sql.append(" ORDER BY p.name ASC") + if (args["tableSorting"] == "ProjectCode") { + sql.append(" ORDER BY p.code ") } else if (args["tableSorting"] == "PercentageASC") { sql.append(" ORDER BY coalesce (round(((sum(t.normalConsumed) + sum(t.otConsumed))/p.totalManhour)*100,2),0) asc") } else if (args["tableSorting"] == "PercentageDESC") { @@ -579,6 +579,7 @@ open class DashboardService( + " where p.teamLead in (:teamIds)" + " and p.status not in ('Pending to Start','Completed','Deleted')" + " group by p.id, p.code, p.name, te.code, s.name, p.totalManhour, milestonePayment.comingPaymentMilestone, taskGroup.expectedStage" +// + " order by p.code " ) // if (args["tableSorting"] == "ProjectName") { @@ -594,6 +595,8 @@ open class DashboardService( sql.append(" ORDER BY coalesce (round(((sum(t.normalConsumed) + sum(t.otConsumed))/p.totalManhour)*100,2),0) asc") } else if (args["tableSorting"] == "PercentageDESC") { sql.append(" ORDER BY coalesce (round(((sum(t.normalConsumed) + sum(t.otConsumed))/p.totalManhour)*100,2),0) desc") + } else { + sql.append(" order by p.code ") } return jdbcDao.queryForList(sql.toString(), args) @@ -1408,6 +1411,7 @@ open class DashboardService( + " left join subsidiary s2 on p.customerSubsidiaryId = s2.id" + " where p.deleted = 0" + " and p.status = 'On-going'" + + " order by p.code" ) if (viewDashboardAuthority() == "self") { @@ -2288,6 +2292,7 @@ open class DashboardService( sql.append(" and p.teamLead = $teamLeadId") } } + sql.append(" order by p.code ") return jdbcDao.queryForList(sql.toString(), args) } @@ -3569,7 +3574,7 @@ open class DashboardService( + " left join project_expense pe on pe.projectId = p.id " + " where p.status = 'On-going' " + (if (args.containsKey("teamId")) "and s.teamId = :teamId" else "") - + " order by p.id " + + " order by p.code " + " ) result ") return jdbcDao.queryForList(sql.toString(), args) } diff --git a/src/main/java/com/ffii/tsms/modules/project/entity/StaffAllocationRepository.kt b/src/main/java/com/ffii/tsms/modules/project/entity/StaffAllocationRepository.kt index 7f3927a..f50f305 100644 --- a/src/main/java/com/ffii/tsms/modules/project/entity/StaffAllocationRepository.kt +++ b/src/main/java/com/ffii/tsms/modules/project/entity/StaffAllocationRepository.kt @@ -7,7 +7,7 @@ import org.springframework.data.jpa.repository.Query import java.time.LocalDate interface StaffAllocationRepository : AbstractRepository { - @Query("SELECT sa.project FROM StaffAllocation sa WHERE sa.staff = ?1 AND sa.project.status = 'On-going'") + @Query("SELECT sa.project FROM StaffAllocation sa WHERE sa.staff = ?1 AND sa.project.status = 'On-going' order by sa.project.code") fun findOnGoingAssignedProjectsByStaff(staff: Staff): List fun findByProject(project: Project): List diff --git a/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt b/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt index 9bf1db2..c3142e4 100644 --- a/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt +++ b/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt @@ -403,30 +403,6 @@ open class ProjectsService( val nonInvoicedAmount = (project.expectedTotalFee?: 0.0) - invoicedAmount return InvoiceData(invoicedAmount, nonInvoicedAmount, receivedAmount) } -// open fun getProjectDashboardDataByProjectId( -// projectId: Long, -// startDate: LocalDate?, -// endDate: LocalDate?, -// ): DashboardData { -// val project = projectRepository.findById(projectId).orElseThrow() -// val manhourExpense = timesheetsService.getManpowerExpenseByProjectId(projectId,startDate,endDate) -// val projectExpense = projectExpenseService.getProjectExpenseByProjectId(projectId,startDate,endDate) -// val invoiceData = getInvoiceDataByProjectId(projectId,startDate,endDate) -// val cumulativeExpenditure = manhourExpense+projectExpense -// val output = DashboardData( -// cumulativeExpenditure, -// manhourExpense, -// projectExpense, -// invoiceData.invoicedAmount, -// invoiceData.nonInvoicedAmount, -// invoiceData.receivedAmount, -// if (invoiceData.invoicedAmount >= manhourExpense+projectExpense) "Positive" else "Negative", -// if (project.expectedTotalFee!! >= cumulativeExpenditure) "Positive" else "Negative", -// if (cumulativeExpenditure > 0.0) invoiceData.invoicedAmount/cumulativeExpenditure else 0.0, -// if (cumulativeExpenditure > 0.0) project.expectedTotalFee!!/cumulativeExpenditure else 0.0 -// ) -// return output -// } open fun getProjectDetails(projectId: Long): EditProjectDetails? { val project = projectRepository.findById(projectId) 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 9a50c3e..16b7f21 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 @@ -31,6 +31,7 @@ import java.time.format.DateTimeParseException import java.time.temporal.ChronoUnit 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 @@ -73,6 +74,7 @@ open class ReportService( "templates/report/AR06_Project Completion Report with Outstanding Accounts Receivable v02.xlsx" 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 fun cellBorderArgs(top: Int, bottom: Int, left: Int, right: Int): MutableMap { var cellBorderArgs = mutableMapOf() @@ -243,6 +245,18 @@ open class ReportService( return outputStream.toByteArray() } + @Throws(IOException::class) + fun generateProjectManhourSummaryReport(args: Map): ByteArray { + val manhourSummary = getManhourSummary(args) + // Generate the Excel report with query results + val workbook: Workbook = createProjectManhourSummaryReport(args, manhourSummary, PROJECT_MANHOUR_SUMMARY) + // Write the workbook to a ByteArrayOutputStream + val outputStream: ByteArrayOutputStream = ByteArrayOutputStream() + workbook.write(outputStream) + workbook.close() + + return outputStream.toByteArray() + } @Throws(IOException::class) fun generateProjectPotentialDelayReport( @@ -2222,6 +2236,37 @@ open class ReportService( ) return jdbcDao.queryForList(sql.toString(), args) } + open fun getManhourSummary(args: Map): List> { + val sql = StringBuilder("select" + + " DATE_FORMAT(t.recordDate, '%b-%Y') AS recordMonth, " + + " t.staffId, " + + " s.name as staff, " + + " p.id as projectId, " + + " p.code as projectCode, " + + " p.name as projectName, " + + " c.name as client, " + + " sum(coalesce(t.normalConsumed, 0) + coalesce(t.otConsumed,0)) as consumed " + + " from timesheet t " + + " left join team_log tl on tl.staffId = t.staffId and t.recordDate >= tl.`from` and (t.recordDate <= tl.`to` or tl.`to` is null) " + + " left join staff s on s.id = t.staffId " + + " left join project p on p.id = t.projectId " + + " left join customer c on c.id = p.customerId " + + " where t.deleted = false " + + " and (tl.teamId is not null and s.teamId is not null) " + + " and t.projectId is not null " + ) + if (args.containsKey(("startDate"))) { + sql.append(" and t.recordDate >= :startDate ") + } + if (args.containsKey(("endDate"))) { + sql.append(" and t.recordDate < :endDate ") + } + if (args.containsKey(("teamId"))) { + sql.append(" and coalesce(tl.teamId, s.teamId) = :teamId ") + } + sql.append(" group by recordMonth, t.staffId, t.projectId, tl.teamId ") + return jdbcDao.queryForList(sql.toString(), args) + } open fun getTotalConsumed(args: Map): List> { val sql = StringBuilder( @@ -3573,7 +3618,6 @@ open class ReportService( return outputStream.toByteArray() } - open fun getLateStartDetails( teamId: Long?, clientId: Long?, @@ -3628,9 +3672,141 @@ open class ReportService( ) return jdbcDao.queryForList(sql.toString(), args) } -// private fun binarySearch(Any[], ) { -// } + fun fontArgs2(sheet: Sheet, fontName: String,isBold: Boolean): MutableMap{ + val font = sheet.workbook.createFont() + font.bold = isBold + font.fontName = fontName + val fontArgs = mutableMapOf( + CellUtil.FONT to font.index, + CellUtil.WRAP_TEXT to true, + ) + return fontArgs + } + fun dataFormatArgs2(accountingStyle: Short): MutableMap { + val dataFormatArgs = mutableMapOf( + CellUtil.DATA_FORMAT to accountingStyle + ) + return dataFormatArgs + } + private fun createProjectManhourSummaryReport( + args: Map, + manhourSummary: List>, + templatePath: String ) + : Workbook + { + val resource = ClassPathResource(templatePath) + val templateInputStream = resource.inputStream + val workbook: Workbook = XSSFWorkbook(templateInputStream) + val sheet: Sheet = workbook.getSheetAt(0) + val accountingStyle = workbook.createDataFormat().getFormat("_(* #,##0.00_);_(* (#,##0.00);_(* \"-\"??_);_(@_)") + fun getMonthsBetweenToColumn(start: LocalDate, end: LocalDate, startValue: Int): Map { + // Get the first day of the start month + val startMonth = start.withDayOfMonth(1) + // Generate a map of months between the start date and the day before the end month + return generateSequence(startMonth) { it.plusMonths(1) } + .takeWhile { it.isBefore(end.withDayOfMonth(1)) } // Exclude the end month +// .takeWhile { it.isBefore(endMonth) || it.isEqual(endMonth) } + .mapIndexed { index, month -> + // Format the month as "MMM-yyyy" + val formattedMonth = month.format(DateTimeFormatter.ofPattern("MMM-yyyy")) + // Calculate the value for this month + formattedMonth to (startValue + index) + } + .toMap() // Convert the list of pairs to a map + } + val startDate = LocalDate.parse(args["startDate"].toString()) + val endDate = LocalDate.parse(args["endDate"].toString()) + val monthList = getMonthsBetweenToColumn(startDate, endDate, 4) + if (monthList.isEmpty()) { + throw IllegalArgumentException("illegal time period") + } + val result = manhourSummary.groupBy { mapOf("staff" to it["staff"], "projectCode" to it["projectCode"], "projectName" to it["projectName"], "client" to it["client"]) } + .map { entry -> + val monthlyConsumption = entry.value.associate { it["recordMonth"] to it["consumed"] } + mapOf("staff" to entry.key["staff"], "projectCode" to entry.key["projectCode"], "projectName" to entry.key["projectName"], "client" to entry.key["client"]) + monthlyConsumption + } + //start from col4 + var rowIndex = 1 + var columnIndex = 1 + var tempRow: Row + var tempCell: Cell + tempRow = getOrCreateRow(sheet, rowIndex) + tempCell = getOrCreateCell(tempRow, columnIndex) + tempCell.setCellValue(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd")).toString()) + //write months header + rowIndex = 5 + columnIndex = 4 + tempRow = getOrCreateRow(sheet, rowIndex) + for (curr in monthList) { + tempCell = getOrCreateCell(tempRow, columnIndex) + alignTopCenter(tempCell) + tempCell.setCellValue(curr.key) + CellUtil.setCellStyleProperties(tempCell, + cellBorderArgs(0,1,0,0) + + fontArgs2(sheet, "Times New Roman",false)) + columnIndex++ + } + //write content + rowIndex = 6 + for ( curr in result) { + tempRow = getOrCreateRow(sheet, rowIndex) + columnIndex = 0 + tempCell = getOrCreateCell(tempRow, columnIndex) + CellUtil.setCellStyleProperties(tempCell, + cellBorderArgs(1,1,1,1) + + fontArgs2(sheet, "Times New Roman",false)) + tempCell.setCellValue(curr["staff"].toString()) + columnIndex = 1 + tempCell = getOrCreateCell(tempRow, columnIndex) + CellUtil.setCellStyleProperties(tempCell, + cellBorderArgs(1,1,1,1) + + fontArgs2(sheet, "Times New Roman",false)) + tempCell.setCellValue(curr["projectCode"].toString()) + columnIndex = 2 + tempCell = getOrCreateCell(tempRow, columnIndex) + CellUtil.setCellStyleProperties(tempCell, + cellBorderArgs(1,1,1,1) + + fontArgs2(sheet, "Times New Roman",false)) + tempCell.setCellValue(curr["projectName"].toString()) + columnIndex = 3 + tempCell = getOrCreateCell(tempRow, columnIndex) + CellUtil.setCellStyleProperties(tempCell, + cellBorderArgs(1,1,1,1) + + fontArgs2(sheet, "Times New Roman",false)) + tempCell.setCellValue(curr["client"].toString()) + for ( month in monthList) { + var manhour = 0.0 + if (curr.containsKey(month.key.toString())) { + manhour = curr[month.key.toString()] as Double + } + columnIndex = month.value + tempCell = getOrCreateCell(tempRow, columnIndex) + tempCell.setCellValue(manhour) + CellUtil.setCellStyleProperties(tempCell, + cellBorderArgs(1,1,1,1) + + fontArgs2(sheet, "Times New Roman",false)) + } + rowIndex++ + } + // total + tempRow = getOrCreateRow(sheet, rowIndex) + columnIndex = monthList.values.firstNotNullOfOrNull { it }!! - 1 + tempCell = getOrCreateCell(tempRow, columnIndex) + tempCell.setCellValue("Total:") + CellUtil.setCellStyleProperties(tempCell, fontArgs2(sheet, "Times New Roman",false)) + setAlignment(tempCell,"top", "right") + for (curr in monthList) { + columnIndex = curr.value + val columnLetter = CellReference.convertNumToColString(columnIndex) + tempCell = getOrCreateCell(tempRow, columnIndex) + CellUtil.setCellStyleProperties(tempCell, + cellBorderArgs(1,1,1,1) + + fontArgs2(sheet, "Times New Roman",false)) + tempCell.cellFormula = "SUM(${columnLetter}7:$columnLetter$rowIndex)" + } + return workbook + } private fun generateTeamsInOutMap( teams: List, desiredTeam: MutableList, 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 4550147..97609fc 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 @@ -307,6 +307,22 @@ class ReportController( return excelReportService.getCostAndExpense(request.clientId, request.teamId, request.type) } + @PostMapping("/ProjectManhourSummary") + @Throws(ServletRequestBindingException::class, IOException::class) + fun getProjectManhourSummary(@RequestBody @Valid request: ProjectManhourSummaryRequest): ResponseEntity { + println("starting") + val args = mapOf( + "startDate" to request.startDate, + "endDate" to request.endDate, + "teamId" to request.teamId, + ) + val reportResult: ByteArray = excelReportService.generateProjectManhourSummaryReport(args) + println("ending") + return ResponseEntity.ok() + .header("filename", "Project Manhour Summary Report - " + LocalDate.now() + ".xlsx") + .body(ByteArrayResource(reportResult)) + } + @PostMapping("/costandexpenseReport") @Throws(ServletRequestBindingException::class, IOException::class) fun getCostAndExpenseReport(@RequestBody @Valid request: costAndExpenseRequest): ResponseEntity { 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 cfa302d..1dae96a 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 @@ -63,4 +63,9 @@ data class ProjectCompletionReport ( data class CrossTeamChargeReportRequest ( val month: String, val teamId: String +) +data class ProjectManhourSummaryRequest ( + val startDate: LocalDate, + val endDate: LocalDate, + val teamId: Any ) \ No newline at end of file diff --git a/src/main/resources/templates/report/Project Manhour Summary.xlsx b/src/main/resources/templates/report/Project Manhour Summary.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..55f5356cc07c7369b12c80e25ee1c2bad06904d7 GIT binary patch literal 9498 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