diff --git a/src/main/java/com/ffii/tsms/modules/project/entity/ProjectRepository.kt b/src/main/java/com/ffii/tsms/modules/project/entity/ProjectRepository.kt index 57d1720..8f367a8 100644 --- a/src/main/java/com/ffii/tsms/modules/project/entity/ProjectRepository.kt +++ b/src/main/java/com/ffii/tsms/modules/project/entity/ProjectRepository.kt @@ -1,10 +1,13 @@ package com.ffii.tsms.modules.project.entity; import com.ffii.core.support.AbstractRepository +import com.ffii.tsms.modules.data.entity.Customer +import com.ffii.tsms.modules.data.entity.Staff import com.ffii.tsms.modules.project.entity.projections.InvoiceInfoSearchInfo import com.ffii.tsms.modules.project.entity.projections.InvoiceSearchInfo import com.ffii.tsms.modules.project.entity.projections.ProjectSearchInfo import org.springframework.data.jpa.repository.Query +import org.springframework.lang.Nullable import java.io.Serializable import java.time.LocalDate @@ -25,8 +28,10 @@ interface ProjectRepository : AbstractRepository { "") fun getLatestCodeNumberByMainProject(isClpProject: Boolean, id: Serializable?): Long? - @Query("SELECT max(case when length(p.code) - length(replace(p.code, '-', '')) > 1 then cast(substring_index(p.code, '-', -1) as long) end) FROM Project p WHERE p.code like ?1 and p.id != ?2") + @Query("SELECT max(case when length(p.code) - length(replace(p.code, '-', '')) > 1 then cast(substring_index(p.code, '-', -1) as long) end) FROM Project p WHERE p.code like %?1% and p.id != ?2") fun getLatestCodeNumberBySubProject(code: String, id: Serializable?): Long? fun findAllByStatusIsNotAndMainProjectIsNull(status: String): List + + fun findAllByTeamLeadAndCustomer(teamLead: Staff, customer: Customer): List } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/project/entity/ProjectTaskRepository.kt b/src/main/java/com/ffii/tsms/modules/project/entity/ProjectTaskRepository.kt index b1b57e0..085ed7e 100644 --- a/src/main/java/com/ffii/tsms/modules/project/entity/ProjectTaskRepository.kt +++ b/src/main/java/com/ffii/tsms/modules/project/entity/ProjectTaskRepository.kt @@ -5,5 +5,6 @@ import com.ffii.core.support.AbstractRepository interface ProjectTaskRepository : AbstractRepository { fun findAllByProject(project: Project): List + fun findAllByProjectIn(projects: List): List fun findByProjectAndTask(project: Project, task: Task): ProjectTask? } \ 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 1c23451..169519b 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 @@ -6,6 +6,7 @@ import com.ffii.tsms.modules.data.entity.Salary import com.ffii.tsms.modules.data.entity.Staff import com.ffii.tsms.modules.data.entity.Team import com.ffii.tsms.modules.project.entity.Invoice +import com.ffii.tsms.modules.project.entity.Milestone import com.ffii.tsms.modules.project.entity.Project import com.ffii.tsms.modules.timesheet.entity.Leave import com.ffii.tsms.modules.timesheet.entity.Timesheet @@ -48,6 +49,7 @@ open class ReportService( private val PandL_REPORT = "templates/report/AR07_Project P&L Report v02.xlsx" private val FINANCIAL_STATUS_REPORT = "templates/report/EX01_Financial Status Report.xlsx" private val PROJECT_CASH_FLOW_REPORT = "templates/report/EX02_Project Cash Flow Report.xlsx" + private val PROJECT_POTENTIAL_DELAY_REPORT = "templates/report/AR02_Delay Report v02.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" private val LATE_START_REPORT = "templates/report/AR01_Late Start Report v01.xlsx" @@ -65,33 +67,35 @@ open class ReportService( val tempList = mutableListOf>() - for (item in financialStatus){ - val normalConsumed = item.getValue("normalConsumed") as Double - val hourlyRate = item.getValue("hourlyRate") as BigDecimal + for (item in financialStatus) { + val normalConsumed = item.getValue("normalConsumed") as Double + val hourlyRate = item.getValue("hourlyRate") as BigDecimal // println("normalConsumed------------- $normalConsumed") // println("hourlyRate------------- $hourlyRate") val manHourRate = normalConsumed.toBigDecimal().multiply(hourlyRate) // println("manHourRate------------ $manHourRate") - val otConsumed = item.getValue("otConsumed") as Double + val otConsumed = item.getValue("otConsumed") as Double val manOtHourRate = otConsumed.toBigDecimal().multiply(hourlyRate).multiply(otFactor) - if(!tempList.any{ it.containsValue(item.getValue("code"))}){ - - tempList.add(mapOf( - "code" to item.getValue("code"), - "description" to item.getValue("description"), - "client" to item.getValue("client"), - "teamLead" to item.getValue("teamLead"), - "planStart" to item.getValue("planStart"), - "planEnd" to item.getValue("planEnd"), - "expectedTotalFee" to item.getValue("expectedTotalFee"), - "normalConsumed" to manHourRate, - "otConsumed" to manOtHourRate, - "issuedAmount" to item.getValue("sumIssuedAmount"), - "paidAmount" to item.getValue("sumPaidAmount"), - )) - }else{ + if (!tempList.any { it.containsValue(item.getValue("code")) }) { + + tempList.add( + mapOf( + "code" to item.getValue("code"), + "description" to item.getValue("description"), + "client" to item.getValue("client"), + "teamLead" to item.getValue("teamLead"), + "planStart" to item.getValue("planStart"), + "planEnd" to item.getValue("planEnd"), + "expectedTotalFee" to item.getValue("expectedTotalFee"), + "normalConsumed" to manHourRate, + "otConsumed" to manOtHourRate, + "issuedAmount" to item.getValue("sumIssuedAmount"), + "paidAmount" to item.getValue("sumPaidAmount"), + ) + ) + } else { // Find the existing Map in the tempList that has the same "code" value val existingMap = tempList.find { it.containsValue(item.getValue("code")) }!! @@ -122,7 +126,32 @@ open class ReportService( dateType: String ): ByteArray { // Generate the Excel report with query results - val workbook: Workbook = createProjectCashFlowReport(project, invoices, timesheets, dateType, PROJECT_CASH_FLOW_REPORT) + val workbook: Workbook = + createProjectCashFlowReport(project, invoices, timesheets, dateType, PROJECT_CASH_FLOW_REPORT) + + // Write the workbook to a ByteArrayOutputStream + val outputStream: ByteArrayOutputStream = ByteArrayOutputStream() + workbook.write(outputStream) + workbook.close() + + return outputStream.toByteArray() + } + + @Throws(IOException::class) + fun generateProjectPotentialDelayReport( + searchedTeam: String, + searchedClient: String, + projects: List, + timesheets: List, + ): ByteArray { + // Generate the Excel report with query results + val workbook: Workbook = createProjectPotentialDelayReport( + searchedTeam, + searchedClient, + projects, + timesheets, + PROJECT_POTENTIAL_DELAY_REPORT + ) // Write the workbook to a ByteArrayOutputStream val outputStream: ByteArrayOutputStream = ByteArrayOutputStream() @@ -224,7 +253,8 @@ open class ReportService( team, customer, project, - LATE_START_REPORT) + LATE_START_REPORT + ) val outputStream: ByteArrayOutputStream = ByteArrayOutputStream() workbook.write(outputStream) workbook.close() @@ -238,20 +268,20 @@ open class ReportService( templatePath: String, projects: List>, teamLeadId: Long - ) : Workbook { + ): Workbook { val resource = ClassPathResource(templatePath) val templateInputStream = resource.inputStream val workbook: Workbook = XSSFWorkbook(templateInputStream) - val accountingStyle = workbook.createDataFormat() .getFormat("_(* #,##0.00_);_(* (#,##0.00);_(* \"-\"??_);_(@_)") + val accountingStyle = workbook.createDataFormat().getFormat("_(* #,##0.00_);_(* (#,##0.00);_(* \"-\"??_);_(@_)") val sheet = workbook.getSheetAt(0) //Set Column 2, 3, 4 to auto width - sheet.setColumnWidth(2, 20*256) - sheet.setColumnWidth(3, 45*256) - sheet.setColumnWidth(4, 15*256) + sheet.setColumnWidth(2, 20 * 256) + sheet.setColumnWidth(3, 45 * 256) + sheet.setColumnWidth(4, 15 * 256) val boldFont = sheet.workbook.createFont() boldFont.bold = true @@ -260,7 +290,7 @@ open class ReportService( boldFontCellStyle.setFont(boldFont) var rowNum = 14 - if (projects.isEmpty()){ + if (projects.isEmpty()) { // Fill the cell in Row 2-12 with thr calculated sum rowNum = 1 val row1: Row = sheet.getRow(rowNum) @@ -270,12 +300,13 @@ open class ReportService( rowNum = 2 val row2: Row = sheet.getRow(rowNum) val row2Cell = row2.createCell(2) - val sql = StringBuilder("select" - + " t.teamLead as id," - + " t.name, t.code " - + " from team t" - + " where t.deleted = false " - + " and t.teamLead = :teamLead " + val sql = StringBuilder( + "select" + + " t.teamLead as id," + + " t.name, t.code " + + " from team t" + + " where t.deleted = false " + + " and t.teamLead = :teamLead " ) val args = mapOf("teamLead" to teamLeadId) val team = jdbcDao.queryForMap(sql.toString(), args).get() @@ -293,14 +324,16 @@ open class ReportService( return workbook } - for(item in projects){ + for (item in projects) { val row: Row = sheet.createRow(rowNum++) val codeCell = row.createCell(0) codeCell.setCellValue(if (item["code"] != null) item.getValue("code").toString() else "N/A") val descriptionCell = row.createCell(1) - descriptionCell.setCellValue(if (item["description"] != null) item.getValue("description").toString() else "N/A") + descriptionCell.setCellValue( + if (item["description"] != null) item.getValue("description").toString() else "N/A" + ) val clientCell = row.createCell(2) clientCell.apply { @@ -334,7 +367,7 @@ open class ReportService( val normalConsumed = item["normalConsumed"]?.let { it as BigDecimal } ?: BigDecimal(0) val otConsumed = item["otConsumed"]?.let { it as BigDecimal } ?: BigDecimal(0) val cumExpenditure = normalConsumed.add(otConsumed) - cumExpenditureCell.apply{ + cumExpenditureCell.apply { setCellValue(cumExpenditure.toDouble()) cellStyle.dataFormat = accountingStyle } @@ -355,7 +388,8 @@ open class ReportService( val uninvoiceCell = row.createCell(11) uninvoiceCell.apply { - cellFormula = " IF(H${rowNum}<=I${rowNum}, H${rowNum}-K${rowNum}, IF(AND(H${rowNum}>I${rowNum}, I${rowNum}I${rowNum}, I${rowNum}>=K${rowNum}), I${rowNum}-K${rowNum}, 0))) " + cellFormula = + " IF(H${rowNum}<=I${rowNum}, H${rowNum}-K${rowNum}, IF(AND(H${rowNum}>I${rowNum}, I${rowNum}I${rowNum}, I${rowNum}>=K${rowNum}), I${rowNum}-K${rowNum}, 0))) " cellStyle = boldFontCellStyle cellStyle.dataFormat = accountingStyle } @@ -367,7 +401,7 @@ open class ReportService( val receivedAmountCell = row.createCell(13) val paidAmount = item["paidAmount"]?.let { it as BigDecimal } ?: BigDecimal(0) - receivedAmountCell.apply{ + receivedAmountCell.apply { setCellValue(paidAmount.toDouble()) cellStyle.dataFormat = accountingStyle } @@ -383,18 +417,18 @@ open class ReportService( val lastRowNum = rowNum + 1 val row: Row = sheet.createRow(rowNum) - for(i in 0..4){ + for (i in 0..4) { val cell = row.createCell(i) - CellUtil.setCellStyleProperty(cell,"borderTop", BorderStyle.THIN) - CellUtil.setCellStyleProperty(cell,"borderBottom", BorderStyle.DOUBLE) + CellUtil.setCellStyleProperty(cell, "borderTop", BorderStyle.THIN) + CellUtil.setCellStyleProperty(cell, "borderBottom", BorderStyle.DOUBLE) } val subTotalCell = row.createCell(5) subTotalCell.apply { setCellValue("Sub-total:") } - CellUtil.setCellStyleProperty(subTotalCell,"borderTop", BorderStyle.THIN) - CellUtil.setCellStyleProperty(subTotalCell,"borderBottom", BorderStyle.DOUBLE) + CellUtil.setCellStyleProperty(subTotalCell, "borderTop", BorderStyle.THIN) + CellUtil.setCellStyleProperty(subTotalCell, "borderBottom", BorderStyle.DOUBLE) val sumTotalFeeCell = row.createCell(6) @@ -402,24 +436,24 @@ open class ReportService( cellFormula = "SUM(G15:G${rowNum})" cellStyle.dataFormat = accountingStyle } - CellUtil.setCellStyleProperty(sumTotalFeeCell,"borderTop", BorderStyle.THIN) - CellUtil.setCellStyleProperty(sumTotalFeeCell,"borderBottom", BorderStyle.DOUBLE) + CellUtil.setCellStyleProperty(sumTotalFeeCell, "borderTop", BorderStyle.THIN) + CellUtil.setCellStyleProperty(sumTotalFeeCell, "borderBottom", BorderStyle.DOUBLE) val sumBudgetCell = row.createCell(7) sumBudgetCell.apply { cellFormula = "SUM(H15:H${rowNum})" cellStyle.dataFormat = accountingStyle } - CellUtil.setCellStyleProperty(sumBudgetCell,"borderTop", BorderStyle.THIN) - CellUtil.setCellStyleProperty(sumBudgetCell,"borderBottom", BorderStyle.DOUBLE) + CellUtil.setCellStyleProperty(sumBudgetCell, "borderTop", BorderStyle.THIN) + CellUtil.setCellStyleProperty(sumBudgetCell, "borderBottom", BorderStyle.DOUBLE) val sumCumExpenditureCell = row.createCell(8) sumCumExpenditureCell.apply { cellFormula = "SUM(I15:I${rowNum})" cellStyle.dataFormat = accountingStyle } - CellUtil.setCellStyleProperty(sumCumExpenditureCell,"borderTop", BorderStyle.THIN) - CellUtil.setCellStyleProperty(sumCumExpenditureCell,"borderBottom", BorderStyle.DOUBLE) + CellUtil.setCellStyleProperty(sumCumExpenditureCell, "borderTop", BorderStyle.THIN) + CellUtil.setCellStyleProperty(sumCumExpenditureCell, "borderBottom", BorderStyle.DOUBLE) val sumBudgetVCell = row.createCell(9) sumBudgetVCell.apply { @@ -427,16 +461,16 @@ open class ReportService( cellStyle = boldFontCellStyle cellStyle.dataFormat = accountingStyle } - CellUtil.setCellStyleProperty(sumBudgetVCell,"borderTop", BorderStyle.THIN) - CellUtil.setCellStyleProperty(sumBudgetVCell,"borderBottom", BorderStyle.DOUBLE) + CellUtil.setCellStyleProperty(sumBudgetVCell, "borderTop", BorderStyle.THIN) + CellUtil.setCellStyleProperty(sumBudgetVCell, "borderBottom", BorderStyle.DOUBLE) val sumIInvoiceCell = row.createCell(10) sumIInvoiceCell.apply { cellFormula = "SUM(K15:K${rowNum})" cellStyle.dataFormat = accountingStyle } - CellUtil.setCellStyleProperty(sumIInvoiceCell,"borderTop", BorderStyle.THIN) - CellUtil.setCellStyleProperty(sumIInvoiceCell,"borderBottom", BorderStyle.DOUBLE) + CellUtil.setCellStyleProperty(sumIInvoiceCell, "borderTop", BorderStyle.THIN) + CellUtil.setCellStyleProperty(sumIInvoiceCell, "borderBottom", BorderStyle.DOUBLE) val sumUInvoiceCell = row.createCell(11) sumUInvoiceCell.apply { @@ -444,20 +478,20 @@ open class ReportService( cellStyle = boldFontCellStyle cellStyle.dataFormat = accountingStyle } - CellUtil.setCellStyleProperty(sumUInvoiceCell,"borderTop", BorderStyle.THIN) - CellUtil.setCellStyleProperty(sumUInvoiceCell,"borderBottom", BorderStyle.DOUBLE) + CellUtil.setCellStyleProperty(sumUInvoiceCell, "borderTop", BorderStyle.THIN) + CellUtil.setCellStyleProperty(sumUInvoiceCell, "borderBottom", BorderStyle.DOUBLE) val lastCpiCell = row.createCell(12) - CellUtil.setCellStyleProperty(lastCpiCell,"borderTop", BorderStyle.THIN) - CellUtil.setCellStyleProperty(lastCpiCell,"borderBottom", BorderStyle.DOUBLE) + CellUtil.setCellStyleProperty(lastCpiCell, "borderTop", BorderStyle.THIN) + CellUtil.setCellStyleProperty(lastCpiCell, "borderBottom", BorderStyle.DOUBLE) val sumRAmountCell = row.createCell(13) sumRAmountCell.apply { cellFormula = "SUM(N15:N${rowNum})" cellStyle.dataFormat = accountingStyle } - CellUtil.setCellStyleProperty(sumRAmountCell,"borderTop", BorderStyle.THIN) - CellUtil.setCellStyleProperty(sumRAmountCell,"borderBottom", BorderStyle.DOUBLE) + CellUtil.setCellStyleProperty(sumRAmountCell, "borderTop", BorderStyle.THIN) + CellUtil.setCellStyleProperty(sumRAmountCell, "borderBottom", BorderStyle.DOUBLE) val sumUnSettleCell = row.createCell(14) sumUnSettleCell.apply { @@ -465,8 +499,8 @@ open class ReportService( cellStyle = boldFontCellStyle cellStyle.dataFormat = accountingStyle } - CellUtil.setCellStyleProperty(sumUnSettleCell,"borderTop", BorderStyle.THIN) - CellUtil.setCellStyleProperty(sumUnSettleCell,"borderBottom", BorderStyle.DOUBLE) + CellUtil.setCellStyleProperty(sumUnSettleCell, "borderTop", BorderStyle.THIN) + CellUtil.setCellStyleProperty(sumUnSettleCell, "borderBottom", BorderStyle.DOUBLE) // Fill the cell in Row 2-12 with thr calculated sum rowNum = 1 @@ -479,7 +513,7 @@ open class ReportService( val row2Cell = row2.createCell(2) if (teamLeadId < 0) { row2Cell.setCellValue("All") - }else{ + } else { row2Cell.apply { cellFormula = "D15" } @@ -640,7 +674,8 @@ open class ReportService( rowIndex = 15 - val dateFormatter = if (dateType == "Date") DateTimeFormatter.ofPattern("yyyy/MM/dd") else DateTimeFormatter.ofPattern("MMM YYYY") + val dateFormatter = + if (dateType == "Date") DateTimeFormatter.ofPattern("yyyy/MM/dd") else DateTimeFormatter.ofPattern("MMM YYYY") val combinedResults = (invoices.map { it.receiptDate } + timesheets.map { it.recordDate }).filterNotNull().sortedBy { it } .map { it.format(dateFormatter) }.distinct() @@ -649,9 +684,11 @@ open class ReportService( .mapValues { (_, timesheetEntries) -> timesheetEntries.map { timesheet -> if (timesheet.normalConsumed != null) { - timesheet.normalConsumed!!.plus(timesheet.otConsumed ?: 0.0).times(timesheet.staff!!.salary.hourlyRate.toDouble()) + timesheet.normalConsumed!!.plus(timesheet.otConsumed ?: 0.0) + .times(timesheet.staff!!.salary.hourlyRate.toDouble()) } else if (timesheet.otConsumed != null) { - timesheet.otConsumed!!.plus(timesheet.normalConsumed ?: 0.0).times(timesheet.staff!!.salary.hourlyRate.toDouble()) + timesheet.otConsumed!!.plus(timesheet.normalConsumed ?: 0.0) + .times(timesheet.staff!!.salary.hourlyRate.toDouble()) } else { 0.0 } @@ -766,6 +803,136 @@ open class ReportService( return workbook } + @Throws(IOException::class) + private fun createProjectPotentialDelayReport( + searchedTeam: String, + searchedClient: String, + projects: List, + timesheets: List, + templatePath: String, + ): Workbook { + // please create a new function for each report template + val resource = ClassPathResource(templatePath) + val templateInputStream = resource.inputStream + val workbook: Workbook = XSSFWorkbook(templateInputStream) + + val sheet: Sheet = workbook.getSheetAt(0) + + var rowIndex = 1 // Assuming the location is in (1,2), which is the report date field + var columnIndex = 2 + sheet.getRow(rowIndex).createCell(columnIndex).apply { + setCellValue(FORMATTED_TODAY) + } + + rowIndex = 2 + sheet.getRow(rowIndex).createCell(columnIndex).apply { + setCellValue(searchedTeam) + } + + rowIndex = 3 + sheet.getRow(rowIndex).createCell(columnIndex).apply { + setCellValue(searchedClient) + } + + val groupedTimesheets = timesheets + .groupBy { timesheetEntry -> + Pair( + timesheetEntry.projectTask?.project?.id, + timesheetEntry.projectTask?.milestone?.id + ) + } + .mapValues { (_, timesheetEntries) -> + timesheetEntries.map { timesheet -> + if (timesheet.normalConsumed != null) { + timesheet.normalConsumed!!.plus(timesheet.otConsumed ?: 0.0) + } else if (timesheet.otConsumed != null) { + timesheet.otConsumed!!.plus(timesheet.normalConsumed ?: 0.0) + } else { + 0.0 + } + } + } + +// groupedTimesheets.entries.forEach { (key, value) -> +// logger.info("key: $key") +// logger.info(key == Pair(1L, 1L)) +// logger.info("value: " + value.sumOf { it }) +// } + + rowIndex = 6 + projects.forEach { project: Project -> + + sheet.createRow(rowIndex).apply { + createCell(0).apply { + setCellValue((rowIndex - 5).toString()) + } + + createCell(1).apply { + setCellValue(project.code) + } + + createCell(2).apply { + setCellValue(project.name) + } + + createCell(3).apply { + val currentTeam = project.teamLead?.team + setCellValue(currentTeam?.code + " - " + currentTeam?.name) + } + + createCell(4).apply { + val currentClient = project.customer + setCellValue(currentClient?.code + " - " + currentClient?.name) + } + + createCell(5).apply { + setCellValue(project.actualStart?.format(DATE_FORMATTER)) + } + + createCell(6).apply { + setCellValue(project.planEnd?.format(DATE_FORMATTER)) + } + } + + project.milestones.forEach { milestone: Milestone -> + logger.info(milestone.id) + + val tempRow = sheet.getRow(rowIndex) ?: sheet.createRow(rowIndex) + rowIndex++ + + tempRow.apply { + createCell(7).apply { + setCellValue(milestone.taskGroup?.name ?: "N/A") + } + + createCell(8).apply { + setCellValue(milestone.endDate?.format(DATE_FORMATTER) ?: "N/A") + } + + createCell(9).apply { + cellStyle.dataFormat = workbook.createDataFormat().getFormat("0.00%") + + if (groupedTimesheets.containsKey(Pair(project.id, milestone.id))) { + val manHoursSpent = groupedTimesheets[Pair(project.id, milestone.id)]!!.sum() + + logger.info("manHoursSpent: $manHoursSpent") + logger.info("milestone.stagePercentAllocation: " + milestone.stagePercentAllocation) + logger.info("project.totalManhour: " + project.totalManhour) + + val resourceUtilization = + manHoursSpent / (milestone.stagePercentAllocation!! / 100 * project.totalManhour!!) + setCellValue(resourceUtilization ?: 0.0) + } else { + setCellValue(0.0) + } + } + } + } + } + + return workbook + } + fun getColumnAlphabet(colIndex: Int): String { val alphabet = StringBuilder() @@ -797,14 +964,16 @@ open class ReportService( // result = timesheet record mapped var result: Map = mapOf() if (timesheets.isNotEmpty()) { - projectList = timesheets.map{ "${it["code"]}\n ${it["name"]}"}.toList().distinct() + projectList = timesheets.map { "${it["code"]}\n ${it["name"]}" }.toList().distinct() result = timesheets.groupBy( { it["id"].toString() }, - { mapOf( - "date" to it["recordDate"], - "normalConsumed" to it["normalConsumed"], - "otConsumed" to it["otConsumed"], - ) } + { + mapOf( + "date" to it["recordDate"], + "normalConsumed" to it["normalConsumed"], + "otConsumed" to it["otConsumed"], + ) + } ) } println("---result---") @@ -888,7 +1057,7 @@ open class ReportService( rowIndex += 1 tempCell = sheet.getRow(rowIndex).createCell(0) - sheet.addMergedRegion(CellRangeAddress(rowIndex,rowIndex , 0, 1)) + sheet.addMergedRegion(CellRangeAddress(rowIndex, rowIndex, 0, 1)) tempCell.setCellValue("Sub-total") tempCell.cellStyle = boldStyle CellUtil.setAlignment(tempCell, HorizontalAlignment.CENTER) @@ -916,7 +1085,7 @@ open class ReportService( tempCell.setCellValue(normalConsumed) tempCell.cellStyle.dataFormat = accountingStyle - sheet.addMergedRegion(CellRangeAddress(rowIndex,rowIndex , 0, 1)) + sheet.addMergedRegion(CellRangeAddress(rowIndex, rowIndex, 0, 1)) // rowIndex += 1 tempCell = sheet.getRow(rowIndex).createCell(0) @@ -936,7 +1105,7 @@ open class ReportService( tempCell.setCellValue("Total Leave Hours") tempCell.cellStyle = boldStyle CellUtil.setAlignment(tempCell, HorizontalAlignment.CENTER) - sheet.addMergedRegion(CellRangeAddress(rowIndex,rowIndex , 0, 1)) + sheet.addMergedRegion(CellRangeAddress(rowIndex, rowIndex, 0, 1)) // cal total leave hour if (leaves.isNotEmpty()) { leaves.forEach { l -> @@ -959,11 +1128,11 @@ open class ReportService( CellUtil.setCellStyleProperties(tempCell, DoubleBorderBottom) tempCell = sheet.getRow(rowIndex).createCell(2) - tempCell.cellFormula = "C${rowIndex-2}+C${rowIndex-1}" + tempCell.cellFormula = "C${rowIndex - 2}+C${rowIndex - 1}" tempCell.cellStyle.dataFormat = accountingStyle CellUtil.setCellStyleProperties(tempCell, DoubleBorderBottom) - sheet.addMergedRegion(CellRangeAddress(rowIndex,rowIndex , 0, 1)) + sheet.addMergedRegion(CellRangeAddress(rowIndex, rowIndex, 0, 1)) ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// rowIndex = 7 columnIndex = 2 @@ -987,13 +1156,13 @@ open class ReportService( tempCell.cellStyle.dataFormat = accountingStyle } } - result.forEach{ (id, list) -> + 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) + (temp[index]["otConsumed"] as Double)) - } + temp.forEachIndexed { index, _ -> + dayInt = temp[index]["date"].toString().toInt() + tempCell = sheet.getRow(dayInt.plus(7)).createCell(columnIndex) + tempCell.setCellValue((temp[index]["normalConsumed"] as Double) + (temp[index]["otConsumed"] as Double)) + } columnIndex++ } } @@ -1010,34 +1179,35 @@ open class ReportService( tempCell.setCellValue(leave["leaveHours"] as Double) } } - ///////////////////////////////////////////////////////// Leave Hours title //////////////////////////////////////////////////////////////////// - tempCell = sheet.getRow(rowIndex).createCell(columnIndex) - tempCell.setCellValue("Leave Hours") - tempCell.cellStyle = boldStyle - CellUtil.setVerticalAlignment(tempCell, VerticalAlignment.CENTER) - CellUtil.setAlignment(tempCell, HorizontalAlignment.CENTER) - CellUtil.setCellStyleProperties(tempCell, ThinBorderBottom) + ///////////////////////////////////////////////////////// Leave Hours title //////////////////////////////////////////////////////////////////// + tempCell = sheet.getRow(rowIndex).createCell(columnIndex) + tempCell.setCellValue("Leave Hours") + tempCell.cellStyle = boldStyle + CellUtil.setVerticalAlignment(tempCell, VerticalAlignment.CENTER) + CellUtil.setAlignment(tempCell, HorizontalAlignment.CENTER) + CellUtil.setCellStyleProperties(tempCell, ThinBorderBottom) columnIndex += 1 - tempCell = sheet.getRow(rowIndex).createCell(columnIndex) - tempCell.setCellValue("Daily Manhour Spent\n(Excluding Leave Hours)") - tempCell.cellStyle = boldStyle - CellUtil.setAlignment(tempCell, HorizontalAlignment.LEFT) - CellUtil.setCellStyleProperties(tempCell, ThinBorderBottom) + tempCell = sheet.getRow(rowIndex).createCell(columnIndex) + tempCell.setCellValue("Daily Manhour Spent\n(Excluding Leave Hours)") + tempCell.cellStyle = boldStyle + CellUtil.setAlignment(tempCell, HorizontalAlignment.LEFT) + CellUtil.setCellStyleProperties(tempCell, ThinBorderBottom) - sheet.addMergedRegion(CellRangeAddress(6,6 , 2, columnIndex)) + sheet.addMergedRegion(CellRangeAddress(6, 6, 2, columnIndex)) ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// rowIndex = 8 if (sheet.getRow(rowIndex - 1).getCell(2).stringCellValue != "Leave Hours") { // cal daily spent manhour for (i in 0 until rowSize) { tempCell = sheet.getRow(rowIndex).createCell(columnIndex) - tempCell.cellFormula = "SUM(${getColumnAlphabet(2)}${rowIndex+1}:${getColumnAlphabet(columnIndex - 2)}${rowIndex+1})" // should columnIndex - 2 + tempCell.cellFormula = + "SUM(${getColumnAlphabet(2)}${rowIndex + 1}:${getColumnAlphabet(columnIndex - 2)}${rowIndex + 1})" // should columnIndex - 2 rowIndex++ } // cal subtotal println(rowIndex) - for (i in 0 ..columnSize ) { // minus last col + for (i in 0..columnSize) { // minus last col println(sheet.getRow(rowIndex).getCell(2 + i)) tempCell = sheet.getRow(rowIndex).getCell(2 + i) ?: sheet.getRow(rowIndex).createCell(2 + i) tempCell.cellFormula = "SUM(${getColumnAlphabet(2 + i)}9:${getColumnAlphabet(2 + i)}${rowIndex})" @@ -1137,7 +1307,7 @@ open class ReportService( result: List>, lowerLimit: Double, templatePath: String - ):Workbook { + ): Workbook { val resource = ClassPathResource(templatePath) val templateInputStream = resource.inputStream val workbook: Workbook = XSSFWorkbook(templateInputStream) @@ -1170,10 +1340,11 @@ open class ReportService( tempCell.setCellValue((index + 1).toDouble()) val keys = obj.keys.toList() keys.forEachIndexed { keyIndex, key -> - tempCell = sheet.getRow(rowIndex).getCell(columnIndex + keyIndex + 1) ?: sheet.getRow(rowIndex).createCell(columnIndex + keyIndex + 1) + tempCell = sheet.getRow(rowIndex).getCell(columnIndex + keyIndex + 1) ?: sheet.getRow(rowIndex) + .createCell(columnIndex + keyIndex + 1) when (obj[key]) { is Double -> tempCell.setCellValue(obj[key] as Double) - else -> tempCell.setCellValue(obj[key] as String ) + else -> tempCell.setCellValue(obj[key] as String) } } rowIndex++ @@ -1191,20 +1362,20 @@ open class ReportService( fillRed.setFillPattern(PatternFormatting.SOLID_FOREGROUND) val cfRules = arrayOf(rule1, rule2) - val regions = arrayOf(CellRangeAddress.valueOf("\$J7:\$K${rowIndex+1}")) + val regions = arrayOf(CellRangeAddress.valueOf("\$J7:\$K${rowIndex + 1}")) sheetCF.addConditionalFormatting(regions, cfRules); return workbook } -//createLateStartReport + //createLateStartReport private fun createLateStartReport( team: Team, customer: Customer, project: List, templatePath: String - ):Workbook{ - + ): Workbook { + val resource = ClassPathResource(templatePath) val templateInputStream = resource.inputStream val workbook: Workbook = XSSFWorkbook(templateInputStream) @@ -1215,9 +1386,9 @@ open class ReportService( val dateCell = sheet.getRow(1)?.getCell(2) ?: sheet.getRow(1).createCell(2) dateCell.setCellValue(formattedToday) - // Start populating project data starting at row 7 - val startRow = 6 // 0-based index for row 7 - val projectDataRow = sheet.createRow(startRow) + // Start populating project data starting at row 7 + val startRow = 6 // 0-based index for row 7 + val projectDataRow = sheet.createRow(startRow) // Styling for cell A1: Font size 16 and bold val headerFont = workbook.createFont().apply { @@ -1277,29 +1448,29 @@ open class ReportService( open fun getFinancialStatus(teamLeadId: Long?): List> { val sql = StringBuilder( " with cte_timesheet as (" - + " Select p.code, s.name as staff, IFNULL(t.normalConsumed, 0) as normalConsumed, IFNULL(t.otConsumed , 0) as otConsumed, s2.hourlyRate" - + " from timesheet t" - + " left join project_task pt on pt.id = t.projectTaskId" - + " left join project p ON p.id = pt.project_id" - + " left join staff s on s.id = t.staffId" - + " left join salary s2 on s.salaryId = s2.salaryPoint" - + " left join team t2 on t2.id = s.teamId" - + " )," - + " cte_invoice as (" - + " select p.code, sum(i.issueAmount) as sumIssuedAmount , sum(i.paidAmount) as sumPaidAmount" - + " from invoice i" - + " left join project p on p.code = i.projectCode" - + " group by p.code" - + " )" - + " select p.code, p.description, c.name as client, concat(t.code, \' - \', t.name) as teamLead, p.planStart , p.planEnd , p.expectedTotalFee," - + " IFNULL(cte_ts.normalConsumed, 0) as normalConsumed, IFNULL(cte_ts.otConsumed, 0) as otConsumed," - + " IFNULL(cte_ts.hourlyRate, 0) as hourlyRate, IFNULL(cte_i.sumIssuedAmount, 0) as sumIssuedAmount, IFNULL(cte_i.sumPaidAmount, 0) as sumPaidAmount" - + " from project p" - + " left join cte_timesheet cte_ts on p.code = cte_ts.code" - + " left join customer c on c.id = p.customerId" - + " left join tsmsdb.team t on t.teamLead = p.teamLead" - + " left join cte_invoice cte_i on cte_i.code = p.code" - + " where p.status = \'On-going\'" + + " Select p.code, s.name as staff, IFNULL(t.normalConsumed, 0) as normalConsumed, IFNULL(t.otConsumed , 0) as otConsumed, s2.hourlyRate" + + " from timesheet t" + + " left join project_task pt on pt.id = t.projectTaskId" + + " left join project p ON p.id = pt.project_id" + + " left join staff s on s.id = t.staffId" + + " left join salary s2 on s.salaryId = s2.salaryPoint" + + " left join team t2 on t2.id = s.teamId" + + " )," + + " cte_invoice as (" + + " select p.code, sum(i.issueAmount) as sumIssuedAmount , sum(i.paidAmount) as sumPaidAmount" + + " from invoice i" + + " left join project p on p.code = i.projectCode" + + " group by p.code" + + " )" + + " select p.code, p.description, c.name as client, concat(t.code, \' - \', t.name) as teamLead, p.planStart , p.planEnd , p.expectedTotalFee," + + " IFNULL(cte_ts.normalConsumed, 0) as normalConsumed, IFNULL(cte_ts.otConsumed, 0) as otConsumed," + + " IFNULL(cte_ts.hourlyRate, 0) as hourlyRate, IFNULL(cte_i.sumIssuedAmount, 0) as sumIssuedAmount, IFNULL(cte_i.sumPaidAmount, 0) as sumPaidAmount" + + " from project p" + + " left join cte_timesheet cte_ts on p.code = cte_ts.code" + + " left join customer c on c.id = p.customerId" + + " left join tsmsdb.team t on t.teamLead = p.teamLead" + + " left join cte_invoice cte_i on cte_i.code = p.code" + + " where p.status = \'On-going\'" ) if (teamLeadId!! > 0) { sql.append(" and p.teamLead = :teamLeadId ") @@ -1313,32 +1484,33 @@ open class ReportService( open fun getTimesheet(args: Map): List> { val sql = StringBuilder( "SELECT" - + " p.id," - + " p.name," - + " p.code," - + " CAST(DATE_FORMAT(t.recordDate, '%d') AS SIGNED) AS recordDate," - + " sum(t.normalConsumed) as normalConsumed," - + " IFNULL(sum(t.otConsumed), 0.0) as otConsumed" - + " from timesheet t" - + " left join project_task pt on t.projectTaskId = pt.id" - + " left join project p on p.id = pt.project_id" - + " where t.staffId = :staffId" - + " group by p.id, t.recordDate" - + " order by p.id, t.recordDate" - + " and t.recordDate BETWEEN :startDate and :endDate" + + " p.id," + + " p.name," + + " p.code," + + " CAST(DATE_FORMAT(t.recordDate, '%d') AS SIGNED) AS recordDate," + + " sum(t.normalConsumed) as normalConsumed," + + " IFNULL(sum(t.otConsumed), 0.0) as otConsumed" + + " from timesheet t" + + " left join project_task pt on t.projectTaskId = pt.id" + + " left join project p on p.id = pt.project_id" + + " where t.staffId = :staffId" + + " group by p.id, t.recordDate" + + " order by p.id, t.recordDate" + + " and t.recordDate BETWEEN :startDate and :endDate" ) return jdbcDao.queryForList(sql.toString(), args) } + open fun getLeaves(args: Map): List> { val sql = StringBuilder( " SELECT " - + " sum(leaveHours) as leaveHours, " - + " CAST(DATE_FORMAT(recordDate, '%d') AS SIGNED) AS recordDate " - + " from `leave` " - + " where staffId = :staffId " - + " and recordDate BETWEEN :startDate and :endDate " - + " group by recordDate " - + " order by recordDate " + + " sum(leaveHours) as leaveHours, " + + " CAST(DATE_FORMAT(recordDate, '%d') AS SIGNED) AS recordDate " + + " from `leave` " + + " where staffId = :staffId " + + " and recordDate BETWEEN :startDate and :endDate " + + " group by recordDate " + + " order by recordDate " ) return jdbcDao.queryForList(sql.toString(), args) } @@ -1396,14 +1568,17 @@ open class ReportService( + " SELECT " + " s.teamId, " + " pt.project_id, " - + " SUM(tns.totalConsumed) AS totalConsumed " + + " SUM(tns.totalConsumed) AS totalConsumed, " + + " sum(tns.totalBudget) as totalBudget " + " FROM ( " + " SELECT " + " t.staffId, " + " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) as totalConsumed, " - + " t.projectTaskId AS taskId " + + " t.projectTaskId AS taskId, " + + " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) * min(sal.hourlyRate) as totalBudget " + " FROM timesheet t " + " LEFT JOIN staff s ON t.staffId = s.id " + + " left join salary sal on sal.salaryPoint = s.salaryId " + " GROUP BY t.staffId, t.projectTaskId " + " ) AS tns " + " INNER JOIN project_task pt ON tns.taskId = pt.id " @@ -1417,7 +1592,7 @@ open class ReportService( + " t.code as team, " + " c.code as client, " + " p.expectedTotalFee as plannedBudget, " - + " COALESCE((tns.totalConsumed * sa.hourlyRate), 0) as actualConsumedBudget, " + + " COALESCE(tns.totalBudget, 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, " @@ -1447,60 +1622,64 @@ open class ReportService( 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) " - "Within Budget" -> "and " + - " ((COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) < 0.9 " + - " and " + - " (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) <= 0.9 " - else -> "" - } - sql.append(statusFilter) + 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) " + + "Within Budget" -> "and " + + " ((COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) < 0.9 " + + " and " + + " (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) <= 0.9 " + + else -> "" + } + sql.append(statusFilter) } return jdbcDao.queryForList(sql.toString(), args) } - open fun getManhoursSpent(projectId: Long, startMonth: String, endMonth: String): List>{ + + open fun getManhoursSpent(projectId: Long, startMonth: String, endMonth: String): List> { val sql = StringBuilder( - " with cte_timesheet as (" - + " Select p.code, s.name as staff, IFNULL(t.normalConsumed, 0) as normalConsumed, IFNULL(t.otConsumed , 0) as otConsumed, s2.salaryPoint, s2.hourlyRate, t.staffId," - + " t.recordDate" - + " from timesheet t" - + " left join project_task pt on pt.id = t.projectTaskId" - + " left join project p ON p.id = pt.project_id" - + " left join staff s on s.id = t.staffId" - + " left join salary s2 on s.salaryId = s2.salaryPoint" - + " left join team t2 on t2.id = s.teamId" - + " )," - + " cte_invoice as (" - + " select p.code, sum(i.issueAmount) as sumIssuedAmount , sum(i.paidAmount) as sumPaidAmount" - + " from invoice i" - + " left join project p on p.code = i.projectCode" - + " group by p.code" - + " )" - + " select p.code, p.description, c.name as client, concat(t.code, \" - \", t.name) as teamLead," - + " IFNULL(cte_ts.normalConsumed, 0) as normalConsumed, IFNULL(cte_ts.otConsumed, 0) as otConsumed, DATE_FORMAT(cte_ts.recordDate, '%Y-%m') as recordDate, " - + " IFNULL(cte_ts.salaryPoint, 0) as salaryPoint, " - + " IFNULL(cte_ts.hourlyRate, 0) as hourlyRate, IFNULL(cte_i.sumIssuedAmount, 0) as sumIssuedAmount, IFNULL(cte_i.sumPaidAmount, 0) as sumPaidAmount," - + " s.name, s.staffId, g.code as gradeCode, g.name as gradeName, t2.code as teamCode, t2.name as teamName" - + " from project p" - + " left join cte_timesheet cte_ts on p.code = cte_ts.code" - + " left join customer c on c.id = p.customerId" - + " left join tsmsdb.team t on t.teamLead = p.teamLead" - + " left join cte_invoice cte_i on cte_i.code = p.code" - + " left join staff s on s.id = cte_ts.staffId" - + " left join grade g on g.id = s.gradeId" - + " left join team t2 on t2.id = s.teamId" - + " where p.deleted = false" - + " and (DATE_FORMAT(cte_ts.recordDate, \'%Y-%m\') >= :startMonth and DATE_FORMAT(cte_ts.recordDate, \'%Y-%m\') <= :endMonth)" - + " and p.id = :projectId" - + " order by g.code, s.name, cte_ts.recordDate" + " with cte_timesheet as (" + + " Select p.code, s.name as staff, IFNULL(t.normalConsumed, 0) as normalConsumed, IFNULL(t.otConsumed , 0) as otConsumed, s2.salaryPoint, s2.hourlyRate, t.staffId," + + " t.recordDate" + + " from timesheet t" + + " left join project_task pt on pt.id = t.projectTaskId" + + " left join project p ON p.id = pt.project_id" + + " left join staff s on s.id = t.staffId" + + " left join salary s2 on s.salaryId = s2.salaryPoint" + + " left join team t2 on t2.id = s.teamId" + + " )," + + " cte_invoice as (" + + " select p.code, sum(i.issueAmount) as sumIssuedAmount , sum(i.paidAmount) as sumPaidAmount" + + " from invoice i" + + " left join project p on p.code = i.projectCode" + + " group by p.code" + + " )" + + " select p.code, p.description, c.name as client, concat(t.code, \" - \", t.name) as teamLead," + + " IFNULL(cte_ts.normalConsumed, 0) as normalConsumed, IFNULL(cte_ts.otConsumed, 0) as otConsumed, DATE_FORMAT(cte_ts.recordDate, '%Y-%m') as recordDate, " + + " IFNULL(cte_ts.salaryPoint, 0) as salaryPoint, " + + " IFNULL(cte_ts.hourlyRate, 0) as hourlyRate, IFNULL(cte_i.sumIssuedAmount, 0) as sumIssuedAmount, IFNULL(cte_i.sumPaidAmount, 0) as sumPaidAmount," + + " s.name, s.staffId, g.code as gradeCode, g.name as gradeName, t2.code as teamCode, t2.name as teamName" + + " from project p" + + " left join cte_timesheet cte_ts on p.code = cte_ts.code" + + " left join customer c on c.id = p.customerId" + + " left join tsmsdb.team t on t.teamLead = p.teamLead" + + " left join cte_invoice cte_i on cte_i.code = p.code" + + " left join staff s on s.id = cte_ts.staffId" + + " left join grade g on g.id = s.gradeId" + + " left join team t2 on t2.id = s.teamId" + + " where p.deleted = false" + + " and (DATE_FORMAT(cte_ts.recordDate, \'%Y-%m\') >= :startMonth and DATE_FORMAT(cte_ts.recordDate, \'%Y-%m\') <= :endMonth)" + + " and p.id = :projectId" + + " order by g.code, s.name, cte_ts.recordDate" ) val args = mapOf( @@ -1535,24 +1714,24 @@ open class ReportService( val staffInfoList = mutableListOf>() println("manHoursSpent------- ${manHoursSpent}") - for(item in manHoursSpent){ - if(info["teamLead"] != item.getValue("teamLead")){ + for (item in manHoursSpent) { + if (info["teamLead"] != item.getValue("teamLead")) { info["teamLead"] = item.getValue("teamLead") } - if(info["client"] != item.getValue("client")){ + if (info["client"] != item.getValue("client")) { info["client"] = item.getValue("client") } - if(info["code"] != item.getValue("code")){ + if (info["code"] != item.getValue("code")) { info["code"] = item.getValue("code") } - if(info["code"] == item["code"] && "paidAmount" !in info){ + if (info["code"] == item["code"] && "paidAmount" !in info) { info["paidAmount"] = item.getValue("sumPaidAmount") } - if(info["description"] != item.getValue("description")){ + if (info["description"] != item.getValue("description")) { info["description"] = item.getValue("description") } val hourlyRate = (item.getValue("hourlyRate") as BigDecimal).toDouble() - if(!staffInfoList.any { it["staffId"] == item["staffId"] && it["name"] == item["name"]}){ + if (!staffInfoList.any { it["staffId"] == item["staffId"] && it["name"] == item["name"] }) { staffInfoList.add( mapOf( "staffId" to item.getValue("staffId"), @@ -1567,13 +1746,13 @@ open class ReportService( "normalConsumed" to item.getValue("normalConsumed"), "otConsumed" to item.getValue("otConsumed"), "totalManHours" to item.getValue("normalConsumed") as Double + item.getValue("otConsumed") as Double, - "manhourExpenditure" to (hourlyRate * item.getValue("normalConsumed") as Double ) - + (hourlyRate * item.getValue("otConsumed") as Double * otFactor) + "manhourExpenditure" to (hourlyRate * item.getValue("normalConsumed") as Double) + + (hourlyRate * item.getValue("otConsumed") as Double * otFactor) ) ) ) ) - }else{ + } else { val existingMap = staffInfoList.find { it.containsValue(item.getValue("staffId")) }!! val updatedMap = existingMap.toMutableMap() val hourlySpentList = updatedMap["hourlySpent"] as MutableList> @@ -1581,10 +1760,18 @@ open class ReportService( if (existingRecord != null) { val existingIndex = hourlySpentList.indexOf(existingRecord) val updatedRecord = existingRecord.toMutableMap() - updatedRecord["normalConsumed"] = (updatedRecord["normalConsumed"] as Double) + item.getValue("normalConsumed") as Double - updatedRecord["otConsumed"] = (updatedRecord["otConsumed"] as Double) + item.getValue("otConsumed") as Double - updatedRecord["totalManHours"] = (updatedRecord["totalManHours"] as Double) + item.getValue("normalConsumed") as Double + item.getValue("otConsumed") as Double - updatedRecord["manhourExpenditure"] = (updatedRecord["manhourExpenditure"] as Double) + (hourlyRate * item.getValue("normalConsumed") as Double) + (hourlyRate * item.getValue("otConsumed") as Double * otFactor) + updatedRecord["normalConsumed"] = + (updatedRecord["normalConsumed"] as Double) + item.getValue("normalConsumed") as Double + updatedRecord["otConsumed"] = + (updatedRecord["otConsumed"] as Double) + item.getValue("otConsumed") as Double + updatedRecord["totalManHours"] = + (updatedRecord["totalManHours"] as Double) + item.getValue("normalConsumed") as Double + item.getValue( + "otConsumed" + ) as Double + updatedRecord["manhourExpenditure"] = + (updatedRecord["manhourExpenditure"] as Double) + (hourlyRate * item.getValue("normalConsumed") as Double) + (hourlyRate * item.getValue( + "otConsumed" + ) as Double * otFactor) hourlySpentList[existingIndex] = updatedRecord } else { hourlySpentList.add( @@ -1594,7 +1781,7 @@ open class ReportService( "otConsumed" to item.getValue("otConsumed"), "totalManHours" to item.getValue("normalConsumed") as Double + item.getValue("otConsumed") as Double, "manhourExpenditure" to (hourlyRate * item.getValue("normalConsumed") as Double) - + (hourlyRate * item.getValue("otConsumed") as Double * otFactor) + + (hourlyRate * item.getValue("otConsumed") as Double * otFactor) ) ) } @@ -1609,7 +1796,7 @@ open class ReportService( tempList.add(mapOf("info" to info)) tempList.add(mapOf("staffInfoList" to staffInfoList)) - println("Only Staff Info List --------------------- ${ tempList.first() { it.containsKey("staffInfoList") }["staffInfoList"]}") + println("Only Staff Info List --------------------- ${tempList.first() { it.containsKey("staffInfoList") }["staffInfoList"]}") println("tempList----------------- $tempList") return tempList @@ -1632,7 +1819,7 @@ open class ReportService( manhoursSpent: List>, startMonth: String, endMonth: String - ): Workbook{ + ): Workbook { val resource = ClassPathResource(templatePath) val templateInputStream = resource.inputStream val workbook: Workbook = XSSFWorkbook(templateInputStream) @@ -1658,10 +1845,11 @@ open class ReportService( val monthFormat = DateTimeFormatter.ofPattern("MMM yyyy", Locale.ENGLISH) - val info:Map = manhoursSpent.first() { it.containsKey("info") }["info"] as Map - val staffInfoList: List> = manhoursSpent.first() { it.containsKey("staffInfoList") }["staffInfoList"] as List> + val info: Map = manhoursSpent.first() { it.containsKey("info") }["info"] as Map + val staffInfoList: List> = + manhoursSpent.first() { it.containsKey("staffInfoList") }["staffInfoList"] as List> - if (staffInfoList.isEmpty()){ + if (staffInfoList.isEmpty()) { val sheet = workbook.getSheetAt(0) var rowNum = 0 rowNum = 1 @@ -1735,7 +1923,7 @@ open class ReportService( row9Cell.setCellValue("${convertStartMonth.format(monthFormat)} - ${convertEndMonth.format(monthFormat)}") rowNum = 10 - for(staff in staffInfoList){ + for (staff in staffInfoList) { // val row: Row = sheet.getRow(rowNum++) ?: sheet.createRow(rowNum++) val row: Row = sheet.getRow(rowNum) ?: sheet.createRow(rowNum) val staffCell = row.getCell(0) ?: row.createCell(0) @@ -1767,7 +1955,7 @@ open class ReportService( } rowNum += 2 val titleRow = sheet.getRow(rowNum) ?: sheet.createRow(rowNum) - sheet.addMergedRegion(CellRangeAddress(rowNum, rowNum, 0, monthRange.size+3)) + sheet.addMergedRegion(CellRangeAddress(rowNum, rowNum, 0, monthRange.size + 3)) val titleCell = titleRow.getCell(0) ?: titleRow.createCell(0) titleCell.apply { setCellValue("Manhours Spent per Month") @@ -1784,7 +1972,7 @@ open class ReportService( } CellUtil.setAlignment(staffCell, HorizontalAlignment.CENTER); CellUtil.setVerticalAlignment(staffCell, VerticalAlignment.CENTER); - CellUtil.setCellStyleProperty(staffCell,"borderBottom", BorderStyle.THIN) + CellUtil.setCellStyleProperty(staffCell, "borderBottom", BorderStyle.THIN) val teamCell = manhoursSpentRow.getCell(1) ?: manhoursSpentRow.createCell(1) teamCell.apply { @@ -1792,44 +1980,44 @@ open class ReportService( } CellUtil.setAlignment(teamCell, HorizontalAlignment.CENTER); CellUtil.setVerticalAlignment(teamCell, VerticalAlignment.CENTER); - CellUtil.setCellStyleProperty(teamCell,"borderBottom", BorderStyle.THIN) + CellUtil.setCellStyleProperty(teamCell, "borderBottom", BorderStyle.THIN) - for ((column, month) in monthRange.withIndex()){ + for ((column, month) in monthRange.withIndex()) { val row: Row = sheet.getRow(rowNum) ?: sheet.createRow(rowNum) - val monthCell = row.getCell(2+column) ?: row.createCell(2+column) + val monthCell = row.getCell(2 + column) ?: row.createCell(2 + column) monthCell.apply { setCellValue(month.getValue("display").toString()) } CellUtil.setAlignment(monthCell, HorizontalAlignment.CENTER); CellUtil.setVerticalAlignment(monthCell, VerticalAlignment.CENTER); - CellUtil.setCellStyleProperty(monthCell,"borderBottom", BorderStyle.THIN) + CellUtil.setCellStyleProperty(monthCell, "borderBottom", BorderStyle.THIN) } val monthColumnEnd = monthRange.size val row: Row = sheet.getRow(rowNum) ?: sheet.createRow(rowNum) - val manhourCell = row.getCell(2+monthColumnEnd) ?: row.createCell(2+monthColumnEnd) + val manhourCell = row.getCell(2 + monthColumnEnd) ?: row.createCell(2 + monthColumnEnd) manhourCell.apply { setCellValue("Manhour Sub-total") } CellUtil.setAlignment(manhourCell, HorizontalAlignment.CENTER); CellUtil.setVerticalAlignment(manhourCell, VerticalAlignment.CENTER); CellUtil.setCellStyleProperty(manhourCell, CellUtil.WRAP_TEXT, true) - CellUtil.setCellStyleProperty(manhourCell,"borderBottom", BorderStyle.THIN) + CellUtil.setCellStyleProperty(manhourCell, "borderBottom", BorderStyle.THIN) - val manhourECell = row.getCell(2+monthColumnEnd+1) ?: row.createCell(2+monthColumnEnd+1) + val manhourECell = row.getCell(2 + monthColumnEnd + 1) ?: row.createCell(2 + monthColumnEnd + 1) manhourECell.apply { setCellValue("Manhour Expenditure (HKD)") } CellUtil.setAlignment(manhourECell, HorizontalAlignment.CENTER); CellUtil.setVerticalAlignment(manhourECell, VerticalAlignment.CENTER); CellUtil.setCellStyleProperty(manhourECell, CellUtil.WRAP_TEXT, true) - CellUtil.setCellStyleProperty(manhourECell,"borderBottom", BorderStyle.THIN) - sheet.setColumnWidth(2+monthColumnEnd+1, 20*256) + CellUtil.setCellStyleProperty(manhourECell, "borderBottom", BorderStyle.THIN) + sheet.setColumnWidth(2 + monthColumnEnd + 1, 20 * 256) // Manhour Spent LOOP println("monthRange--------------- ${monthRange}\n") rowNum += 1 - for(staff in staffInfoList){ + for (staff in staffInfoList) { val row: Row = sheet.getRow(rowNum) ?: sheet.createRow(rowNum) val staffCell = row.getCell(0) ?: row.createCell(0) var manhourExpenditure = 0.0 @@ -1843,10 +2031,10 @@ open class ReportService( } CellUtil.setCellStyleProperty(teamCell, CellUtil.WRAP_TEXT, true) - for(i in 0 until monthRange.size){ - val cell = row.getCell(i+2) ?: row.createCell(i+2) + for (i in 0 until monthRange.size) { + val cell = row.getCell(i + 2) ?: row.createCell(i + 2) cell.setCellValue(0.0) - for(hourlySpent in staff.getValue("hourlySpent") as List>){ + for (hourlySpent in staff.getValue("hourlySpent") as List>) { cell.apply { if (hourlySpent.getValue("recordDate") == monthRange[i].getValue("date").toString()) { setCellValue(hourlySpent.getValue("totalManHours") as Double) @@ -1859,13 +2047,13 @@ open class ReportService( CellUtil.setVerticalAlignment(cell, VerticalAlignment.CENTER); } - val manhourCell = row.getCell(2+monthRange.size) ?: row.createCell(2+monthRange.size) + val manhourCell = row.getCell(2 + monthRange.size) ?: row.createCell(2 + monthRange.size) manhourCell.apply { - cellFormula="SUM(C${rowNum+1}:${getColumnAlphabet(1+monthRange.size)}${rowNum+1})" + cellFormula = "SUM(C${rowNum + 1}:${getColumnAlphabet(1 + monthRange.size)}${rowNum + 1})" cellStyle.dataFormat = accountingStyle } - val manhourECell = row.getCell(3+monthRange.size) ?: row.createCell(3+monthRange.size) + val manhourECell = row.getCell(3 + monthRange.size) ?: row.createCell(3 + monthRange.size) manhourECell.apply { setCellValue(manhourExpenditure) cellStyle.dataFormat = accountingStyle @@ -1887,19 +2075,19 @@ open class ReportService( rowNum += 1 val totalManhourERow: Row = sheet.getRow(rowNum) ?: sheet.createRow(rowNum) - val startRow = 15+staffInfoList.size - val lastColumnIndex = getColumnAlphabet(3+monthRange.size) + val startRow = 15 + staffInfoList.size + val lastColumnIndex = getColumnAlphabet(3 + monthRange.size) val totalManhourETitleCell = totalManhourERow.getCell(0) ?: totalManhourERow.createCell(0) totalManhourETitleCell.apply { setCellValue("Total Manhour Expenditure (HKD)") } val totalManhourECell = totalManhourERow.getCell(1) ?: totalManhourERow.createCell(1) totalManhourECell.apply { - cellFormula = "SUM(${lastColumnIndex}${startRow}:${lastColumnIndex}${startRow+staffInfoList.size})" + cellFormula = "SUM(${lastColumnIndex}${startRow}:${lastColumnIndex}${startRow + staffInfoList.size})" cellStyle.dataFormat = accountingStyle } - CellUtil.setCellStyleProperty(totalManhourETitleCell,"borderBottom", BorderStyle.THIN) - CellUtil.setCellStyleProperty(totalManhourECell,"borderBottom", BorderStyle.THIN) + CellUtil.setCellStyleProperty(totalManhourETitleCell, "borderBottom", BorderStyle.THIN) + CellUtil.setCellStyleProperty(totalManhourECell, "borderBottom", BorderStyle.THIN) rowNum += 1 val pandlRow: Row = sheet.getRow(rowNum) ?: sheet.createRow(rowNum) @@ -1909,11 +2097,11 @@ open class ReportService( } val panlCell = pandlRow.getCell(1) ?: pandlRow.createCell(1) panlCell.apply { - cellFormula = "B${rowNum-1}-B${rowNum}" + cellFormula = "B${rowNum - 1}-B${rowNum}" cellStyle.dataFormat = accountingStyle } - CellUtil.setCellStyleProperty(panlCellTitle,"borderBottom", BorderStyle.DOUBLE) - CellUtil.setCellStyleProperty(panlCell,"borderBottom", BorderStyle.DOUBLE) + CellUtil.setCellStyleProperty(panlCellTitle, "borderBottom", BorderStyle.DOUBLE) + CellUtil.setCellStyleProperty(panlCell, "borderBottom", BorderStyle.DOUBLE) return workbook } 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 a23e649..70db023 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 @@ -2,11 +2,9 @@ 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.* //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.* @@ -35,11 +33,12 @@ import java.time.LocalDate import java.net.URLEncoder import java.time.format.DateTimeFormatter 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.project.entity.Project import com.ffii.tsms.modules.report.web.model.* +import com.ffii.tsms.modules.timesheet.entity.Timesheet +import org.springframework.data.domain.Example +import org.springframework.data.domain.ExampleMatcher @RestController @RequestMapping("/reports") @@ -87,6 +86,34 @@ class ReportController( .body(ByteArrayResource(reportResult)) } + @PostMapping("/ProjectPotentialDelayReport") + @Throws(ServletRequestBindingException::class, IOException::class) + fun getProjectPotentialDelayReport(@RequestBody @Valid request: ProjectPotentialDelayReportRequest): ResponseEntity { + + val team = if (request.teamId.lowercase() == "all") null else teamRepository.findById(request.teamId.toLong()).orElse(null) + val searchedTeam = if (team == null) "All" else team.code + " - " +team.name + val client = if (request.clientId.lowercase() == "all") null else customerRepository.findById(request.clientId.toLong()).orElse(null) + val searchedClient = if (client == null) "All" else client.code + " - " +client.name + + val matcher = ExampleMatcher.matching().withIgnoreNullValues() + val exampleQuery = Example.of(Project().apply { + // org.springframework.dao.InvalidDataAccessApiUsageException: Path 'teamLead.team.staff' from root Project must not span a cyclic property reference + // [{ com.ffii.tsms.modules.project.entity.Project@6847e037 }] -teamLead-> [{ com.ffii.tsms.modules.data.entity.Staff@2a4c488b }] -team-> [{ com.ffii.tsms.modules.data.entity.Team@a09acb5 }] -staff-> [{ com.ffii.tsms.modules.data.entity.Staff@2a4c488b }] + // teamLead = team?.staff + customer = client + status = "On-going" + }, matcher) + + val projects = if (team == null) projectRepository.findAll(exampleQuery) else projectRepository.findAll(exampleQuery).filter { it.teamLead == team.staff } + + val projectTasks = projectTaskRepository.findAllByProjectIn(projects) + val timesheets = timesheetRepository.findAllByProjectTaskIn(projectTasks) + val reportResult: ByteArray = excelReportService.generateProjectPotentialDelayReport(searchedTeam, searchedClient, projects, timesheets) + return ResponseEntity.ok() + .header("filename", "Project Potential Delay Report - " + LocalDate.now() + ".xlsx") + .body(ByteArrayResource(reportResult)) + } + @PostMapping("/StaffMonthlyWorkHourAnalysisReport") @Throws(ServletRequestBindingException::class, IOException::class) fun StaffMonthlyWorkHourAnalysisReport(@RequestBody @Valid request: StaffMonthlyWorkHourAnalysisReportRequest): 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 287e301..6747af7 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 @@ -24,6 +24,11 @@ data class ProjectCashFlowReportRequest ( val dateType: String, // "Date", "Month" ) +data class ProjectPotentialDelayReportRequest ( + val teamId: String, + val clientId: String, +) + data class StaffMonthlyWorkHourAnalysisReportRequest ( val id: Long, val yearMonth: YearMonth diff --git a/src/main/resources/templates/report/AR02_Delay Report v02.xlsx b/src/main/resources/templates/report/AR02_Delay Report v02.xlsx index fe0d493..1d84434 100644 Binary files a/src/main/resources/templates/report/AR02_Delay Report v02.xlsx and b/src/main/resources/templates/report/AR02_Delay Report v02.xlsx differ