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 0000000..55f5356 Binary files /dev/null and b/src/main/resources/templates/report/Project Manhour Summary.xlsx differ