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 df7cd45..7a55c60 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 @@ -38,5 +38,8 @@ interface ProjectRepository : AbstractRepository { fun findByCode(code: String): Project? + @Query("SELECT p.id FROM Project p WHERE substring_index(substring_index(p.code, '-', 2), '-', -1) like concat('%', substring_index(substring_index(?1, '-', 2), '-', -1), '%')") + fun checkMainProjectByCodeLike(code: String): List? + fun findProjectPlanStartEndByIdAndDeletedFalse(id: Long): ProjectPlanStartEnd } \ No newline at end of file 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 36ee1e3..90fefea 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 @@ -200,10 +200,10 @@ open class ProjectsService( .orElseThrow() else Project() val duplicateProject = - if (request.projectCode != null) projectRepository.findByCode(request.projectCode) else null + if (request.projectCode != null && request.mainProjectId == null) projectRepository.checkMainProjectByCodeLike(request.projectCode) else null //check duplicate project - if (duplicateProject != null && !duplicateProject.deleted && duplicateProject.id?.equals(request.projectId) == false) { + if (!duplicateProject.isNullOrEmpty() && !duplicateProject.contains(request.projectId)) { return NewProjectResponse( id = request.projectId, code = request.projectCode, 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 69c651a..4f06ab7 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 @@ -684,7 +684,7 @@ open class ReportService( val row10: Row = sheet.getRow(rowNum) val cell6 = row10.createCell(2) cell6.apply { - cellFormula = "O${lastRowNum}" + cellFormula = "P${lastRowNum}" cellStyle.dataFormat = accountingStyle } @@ -692,7 +692,7 @@ open class ReportService( val row11: Row = sheet.getRow(rowNum) val cell7 = row11.createCell(2) cell7.apply { - cellFormula = "P${lastRowNum}" + cellFormula = "Q${lastRowNum}" cellStyle.dataFormat = accountingStyle } @@ -1182,7 +1182,7 @@ open class ReportService( println("----leaves-----") println(leaves) // result = timesheet record mapped - var result: Map = mapOf() + var result: Map = mapOf() if (timesheets.isNotEmpty()) { projectList = timesheets.map { "${it["code"]}\n ${it["name"]}" }.toList().distinct() result = timesheets.groupBy( @@ -1333,7 +1333,7 @@ open class ReportService( // } // } if (totalConsumed.isNotEmpty()) { - totalConsumed.forEach{ t -> + totalConsumed.forEach { t -> val total = t["totalConsumed"] as Double if (total > 8.0) { normalConsumed += 8.0 @@ -1964,16 +1964,18 @@ open class ReportService( ) return jdbcDao.queryForList(sql.toString(), args) } + open fun getTotalConsumed(args: Map): List> { - val sql = StringBuilder("SELECT" - + " CAST(DATE_FORMAT(t.recordDate, '%d') AS SIGNED) AS recordDate, " - + " sum(t.normalConsumed) + sum(t.otConsumed) as totalConsumed " - + " from timesheet t " - + " left join project p on p.id = t.projectId " - + " where t.staffId = :staffId " - + " and t.recordDate BETWEEN :startDate and :endDate " - + " group by t.recordDate " - + " order by t.recordDate; " + val sql = StringBuilder( + "SELECT" + + " CAST(DATE_FORMAT(t.recordDate, '%d') AS SIGNED) AS recordDate, " + + " sum(t.normalConsumed) + sum(t.otConsumed) as totalConsumed " + + " from timesheet t " + + " left join project p on p.id = t.projectId " + + " where t.staffId = :staffId " + + " and t.recordDate BETWEEN :startDate and :endDate " + + " group by t.recordDate " + + " order by t.recordDate; " ) return jdbcDao.queryForList(sql.toString(), args) } @@ -2054,42 +2056,43 @@ open class ReportService( } open fun getProjectResourceOverconsumptionReport(args: Map): List> { - val sql = StringBuilder("SELECT" - + " p.code, " - + " p.name, " - + " tm.code as team, " - + " concat(c.code, ' - ',c.name) as client, " - + " COALESCE(concat(ss.code, ' - ', ss.name), 'N/A') as subsidiary, " - + " p.expectedTotalFee * 0.8 as plannedBudget, " - + " sum(t.consumedBudget) as actualConsumedBudget, " - + " COALESCE(p.totalManhour, 0) as plannedManhour, " - + " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) as actualConsumedManhour, " - + " sum(t.consumedBudget) / p.expectedTotalFee * 0.8 as budgetConsumptionRate, " - + " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) / COALESCE(p.totalManhour, 0) as manhourConsumptionRate, " - + " case " - + " when (sum(t.consumedBudget) / p.expectedTotalFee) >= :lowerLimit and (sum(t.consumedBudget) / p.expectedTotalFee) <= 1 " - + " or (sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) / COALESCE(p.totalManhour, 0)) >= :lowerLimit and (sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) / COALESCE(p.totalManhour, 0)) <= 1 " - + " then 'Potential Overconsumption' " - + " when (sum(t.consumedBudget) / p.expectedTotalFee) >= 1 " - + " or (sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) / COALESCE(p.totalManhour, 0)) >= 1 " - + " then 'Overconsumption' " - + " else 'Within Budget' " - + " END as status " - + " from " - + " (SELECT " - + " t.*, " - + " (t.normalConsumed + COALESCE(t.otConsumed, 0)) * sal.hourlyRate as consumedBudget " - + " from timesheet t " - + " left join staff s on s.id = t.staffId " - + " left join salary sal on sal.salaryPoint = s.salaryId ) t " - + " left join project p on p.id = t.projectId " - + " left join team tm on p.teamLead = tm.teamLead " - + " left join customer c on c.id = p.customerId " - + " LEFT JOIN subsidiary ss on p.customerSubsidiaryId = ss.id " - + " WHERE p.deleted = false " - + " and p.status = 'On-going' " + val sql = StringBuilder( + "SELECT" + + " p.code, " + + " p.name, " + + " tm.code as team, " + + " concat(c.code, ' - ',c.name) as client, " + + " COALESCE(concat(ss.code, ' - ', ss.name), 'N/A') as subsidiary, " + + " p.expectedTotalFee * 0.8 as plannedBudget, " + + " sum(t.consumedBudget) as actualConsumedBudget, " + + " COALESCE(p.totalManhour, 0) as plannedManhour, " + + " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) as actualConsumedManhour, " + + " sum(t.consumedBudget) / p.expectedTotalFee * 0.8 as budgetConsumptionRate, " + + " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) / COALESCE(p.totalManhour, 0) as manhourConsumptionRate, " + + " case " + + " when (sum(t.consumedBudget) / p.expectedTotalFee) >= :lowerLimit and (sum(t.consumedBudget) / p.expectedTotalFee) <= 1 " + + " or (sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) / COALESCE(p.totalManhour, 0)) >= :lowerLimit and (sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) / COALESCE(p.totalManhour, 0)) <= 1 " + + " then 'Potential Overconsumption' " + + " when (sum(t.consumedBudget) / p.expectedTotalFee) >= 1 " + + " or (sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) / COALESCE(p.totalManhour, 0)) >= 1 " + + " then 'Overconsumption' " + + " else 'Within Budget' " + + " END as status " + + " from " + + " (SELECT " + + " t.*, " + + " (t.normalConsumed + COALESCE(t.otConsumed, 0)) * sal.hourlyRate as consumedBudget " + + " from timesheet t " + + " left join staff s on s.id = t.staffId " + + " left join salary sal on sal.salaryPoint = s.salaryId ) t " + + " left join project p on p.id = t.projectId " + + " left join team tm on p.teamLead = tm.teamLead " + + " left join customer c on c.id = p.customerId " + + " LEFT JOIN subsidiary ss on p.customerSubsidiaryId = ss.id " + + " WHERE p.deleted = false " + + " and p.status = 'On-going' " ) - var statusFilter: String = "" + var statusFilter: String = "" if (args != null) { if (args.containsKey("teamId")) sql.append(" and t.id = :teamId") @@ -2104,14 +2107,16 @@ open class ReportService( " and (sum(t.consumedBudget) / p.expectedTotalFee) <= 1 " + " or (sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) / COALESCE(p.totalManhour, 0)) >= :lowerLimit " + " and (sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) / COALESCE(p.totalManhour, 0)) <= 1 " + "All" -> " having " + " (sum(t.consumedBudget) / p.expectedTotalFee) >= :lowerLimit " + " or (sum(t.consumedBudget) / p.expectedTotalFee) >= :lowerLimit " + else -> "" } - } - sql.append(" group by p.code, p.name, tm.code, c.code, c.name, ss.code, ss.name,p.expectedTotalFee, p.totalManhour, p.expectedTotalFee ") - sql.append(statusFilter) + } + sql.append(" group by p.code, p.name, tm.code, c.code, c.name, ss.code, ss.name,p.expectedTotalFee, p.totalManhour, p.expectedTotalFee ") + sql.append(statusFilter) return jdbcDao.queryForList(sql.toString(), args) } @@ -2212,7 +2217,7 @@ open class ReportService( val staffInfoList = mutableListOf>() // println("manHoursSpent------- ${manHoursSpent}") - for (financialYear in financialYears){ + for (financialYear in financialYears) { // println("${financialYear.start.year}-${financialYear.start.monthValue} - ${financialYear.end.year}-${financialYear.end.monthValue}") println("financialYear--------- ${financialYear.start} - ${financialYear.end}") } @@ -2336,7 +2341,11 @@ open class ReportService( // For Calculating the Financial Year data class FinancialYear(val start: YearMonth, val end: YearMonth, val hourlyRate: Double) - fun getFinancialYearDates(startDate: YearMonth, endDate: YearMonth, financialYearStartMonth: Int): Array { + fun getFinancialYearDates( + startDate: YearMonth, + endDate: YearMonth, + financialYearStartMonth: Int + ): Array { val financialYearDates = mutableListOf() var currentYear = startDate.year @@ -3101,9 +3110,9 @@ open class ReportService( } // if (timesheets.isNotEmpty()) { - val combinedTeamCodeColNumber = grades.size * 2 + var combinedTeamCodeColNumber = grades.size * 2 val sortedGrades = grades.sortedBy { it.id } - var sortedTeams = teams.sortedBy { it.id }.toMutableList() + val sortedTeams = teams.sortedBy { it.id }.toMutableList() if (teamId.lowercase() != "all") { val teamIndex = sortedTeams.indexOfFirst { it.id == teamId.toLong() } sortedTeams.add(0, sortedTeams.removeAt(teamIndex)) @@ -3205,12 +3214,20 @@ open class ReportService( createCell(columnIndex++).apply { setCellValue("Total Manhour by Team") - cellStyle = boldFontWithBorderStyle + val cloneStyle = workbook.createCellStyle() + cloneStyle.cloneStyleFrom(boldFontWithBorderStyle) + cellStyle = cloneStyle.apply { + alignment = HorizontalAlignment.CENTER + } } createCell(columnIndex).apply { setCellValue("Total Cost Adjusted by Salary Point by Team") - cellStyle = boldFontWithBorderStyle + val cloneStyle = workbook.createCellStyle() + cloneStyle.cloneStyleFrom(boldFontWithBorderStyle) + cellStyle = cloneStyle.apply { + alignment = HorizontalAlignment.CENTER + } } } @@ -3342,39 +3359,241 @@ open class ReportService( // } conditionalFormattingNegative(sheet) - // -------------------------- sheet 1 (By Detail) -------------------------- // -// sheet = workbook.getSheetAt(1) -// -// val groupedTimesheets2 = timesheets -// .filter { it.project?.teamLead?.team?.id != it.staff?.team?.id } -// .groupBy { timesheetEntry -> -// Pair( -// timesheetEntry.project?.id, -// timesheetEntry.staff?.id -// ) -// } -// .mapValues { (_, timesheetEntries) -> -// timesheetEntries.map { timesheet -> -// if (timesheet.normalConsumed != null) { -// mutableMapOf().apply { -// this["manHour"] = timesheet.normalConsumed!!.plus(timesheet.otConsumed ?: 0.0) -// this["salary"] = timesheet.normalConsumed!!.plus(timesheet.otConsumed ?: 0.0) -// .times(timesheet.staff!!.salary.hourlyRate.toDouble()) -// } -// } else if (timesheet.otConsumed != null) { -// mutableMapOf().apply { -// this["manHour"] = timesheet.otConsumed!!.plus(timesheet.normalConsumed ?: 0.0) -// this["salary"] = timesheet.otConsumed!!.plus(timesheet.normalConsumed ?: 0.0) -// .times(timesheet.staff!!.salary.hourlyRate.toDouble()) -// } -// } else { -// mutableMapOf().apply { -// this["manHour"] = 0.0 -// this["salary"] = 0.0 -// } -// } -// } -// } + // -------------------------- sheet 1 (Individual) -------------------------- // + sheet = workbook.getSheetAt(1) + + rowIndex = 1 + columnIndex = 2 + sheet.getRow(rowIndex).createCell(columnIndex).apply { + setCellValue(convertReportMonth) + } + + val groupedTimesheetsIndividual = timesheets + .filter { it.project?.teamLead?.team?.id != it.staff?.team?.id } + .groupBy { timesheetEntry -> + Pair( + timesheetEntry.project?.id, + timesheetEntry.staff?.id + ) + } + .mapValues { (_, timesheetEntries) -> + timesheetEntries.map { timesheet -> + if (timesheet.normalConsumed != null) { + mutableMapOf().apply { + this["manHour"] = timesheet.normalConsumed!!.plus(timesheet.otConsumed ?: 0.0) + this["salary"] = timesheet.normalConsumed!!.plus(timesheet.otConsumed ?: 0.0) + .times(timesheet.staff!!.salary.hourlyRate.toDouble()) + } + } else if (timesheet.otConsumed != null) { + mutableMapOf().apply { + this["manHour"] = timesheet.otConsumed!!.plus(timesheet.normalConsumed ?: 0.0) + this["salary"] = timesheet.otConsumed!!.plus(timesheet.normalConsumed ?: 0.0) + .times(timesheet.staff!!.salary.hourlyRate.toDouble()) + } + } else { + mutableMapOf().apply { + this["manHour"] = 0.0 + this["salary"] = 0.0 + } + } + } + } + + if (sortedTeams.isNotEmpty() && sortedTeams.size > 1) { + rowIndex = 3 + sortedTeams.forEach { team: Team -> + // not his/her team staffs + val staffs = timesheets + .filter { it.project?.teamLead?.team?.id != it.staff?.team?.id && (it.project?.teamLead?.id != team.staff.id || it.staff?.team?.id != team.id) } + .mapNotNull { it.staff } + .sortedBy { it.staffId } + .distinct() + + // his/her team projects + val projects = timesheets + .filter { it.project?.teamLead?.team?.id != it.staff?.team?.id && it.project?.teamLead?.id == team.staff.id } + .mapNotNull { it.project } + .sortedByDescending { it.code } + .distinct() + + // Team + if (!projects.isNullOrEmpty()) { + sheet.createRow(rowIndex++).apply { + createCell(0).apply { + setCellValue("Team to be charged:") + cellStyle = boldFontWithBorderStyle + CellUtil.setAlignment(this, HorizontalAlignment.LEFT) + } + + val rangeAddress = CellRangeAddress(this.rowNum, this.rowNum, 1, 2 + staffs.size) + sheet.addMergedRegion(rangeAddress) + RegionUtil.setBorderTop(BorderStyle.THIN, rangeAddress, sheet) + RegionUtil.setBorderLeft(BorderStyle.THIN, rangeAddress, sheet) + RegionUtil.setBorderRight(BorderStyle.THIN, rangeAddress, sheet) + RegionUtil.setBorderBottom(BorderStyle.THIN, rangeAddress, sheet) + + createCell(1).apply { + setCellValue(team.code) + + val cloneStyle = workbook.createCellStyle() + cloneStyle.cloneStyleFrom(normalFontWithBorderStyle) + cellStyle = cloneStyle.apply { + alignment = HorizontalAlignment.CENTER + } + } + + } + + // Staffs + sheet.createRow(rowIndex++).apply { + columnIndex = 0 + createCell(columnIndex++).apply { + setCellValue("") + cellStyle = boldFontWithBorderStyle + } + + + staffs.forEach { staff: Staff -> + createCell(columnIndex++).apply { + setCellValue("${staff.staffId} - ${staff.name} (${staff.team.code})") + val cloneStyle = workbook.createCellStyle() + cloneStyle.cloneStyleFrom(boldFontWithBorderStyle) + cellStyle = cloneStyle.apply { + alignment = HorizontalAlignment.CENTER + } + + sheet.setColumnWidth(this.columnIndex, 50 * 256) + } + } + + createCell(columnIndex++).apply { + setCellValue("Total Manhour by Project") + val cloneStyle = workbook.createCellStyle() + cloneStyle.cloneStyleFrom(boldFontWithBorderStyle) + cellStyle = cloneStyle.apply { + alignment = HorizontalAlignment.CENTER + } + sheet.setColumnWidth(this.columnIndex, 50 * 256) + } + + createCell(columnIndex).apply { + setCellValue("Total Cost Adjusted by Salary Point by Project") + val cloneStyle = workbook.createCellStyle() + cloneStyle.cloneStyleFrom(boldFontWithBorderStyle) + cellStyle = cloneStyle.apply { + alignment = HorizontalAlignment.CENTER + } + sheet.setColumnWidth(this.columnIndex, 50 * 256) + } + } + + // Project + Manhour + val startRow = rowIndex + 1 + var endRow = rowIndex + projects.forEach { project: Project -> + if (teamId.lowercase() == "all" || teamId.toLong() == project.teamLead?.team?.id || teamId.toLong() == team.id) { + if (team.id == project.teamLead?.team?.id) { + endRow++ + sheet.createRow(rowIndex++).apply { + columnIndex = 0 + createCell(columnIndex++).apply { + setCellValue("${project.code}: ${project.name}") + val cloneStyle = workbook.createCellStyle() + cloneStyle.cloneStyleFrom(normalFontWithBorderStyle) + cellStyle = cloneStyle.apply { + alignment = HorizontalAlignment.LEFT + } + } + + var totalSalary = 0.0 + staffs.forEach { staff: Staff -> + logger.info("Staff: ${staff.staffId}") + createCell(columnIndex++).apply { + setCellValue( + groupedTimesheetsIndividual[Pair( + project.id, + staff.id, + )]?.sumOf { it.getValue("manHour") } ?: 0.0) + + totalSalary += groupedTimesheetsIndividual[Pair( + project.id, + staff.id + )]?.sumOf { it.getValue("salary") } ?: 0.0 + + val cloneStyle = workbook.createCellStyle() + cloneStyle.cloneStyleFrom(normalFontWithBorderStyle) + cellStyle = cloneStyle.apply { + dataFormat = accountingStyle + } + } + } + + createCell(columnIndex++).apply { + val lastCellLetter = CellReference.convertNumToColString(this.columnIndex - 1) + cellFormula = "sum(B${this.rowIndex + 1}:${lastCellLetter}${this.rowIndex + 1})" + + val cloneStyle = workbook.createCellStyle() + cloneStyle.cloneStyleFrom(boldFontWithBorderStyle) + cellStyle = cloneStyle.apply { + dataFormat = accountingStyle + } + } + + createCell(columnIndex).apply { + setCellValue(totalSalary) + val cloneStyle = workbook.createCellStyle() + cloneStyle.cloneStyleFrom(boldFontWithBorderStyle) + cellStyle = cloneStyle.apply { + dataFormat = accountingStyle + } + } + } + } + } + } + + // Total Manhour & Cost + sheet.createRow(rowIndex).apply { + columnIndex = 0 + createCell(columnIndex++).apply { + setCellValue("") + cellStyle = boldFontWithBorderStyle + } + + staffs.forEach { staff: Staff -> + createCell(columnIndex++).apply { + setCellValue("") + cellStyle = boldFontWithBorderStyle + } + } + + createCell(columnIndex++).apply { + val lastCellLetter = CellReference.convertNumToColString(this.columnIndex) + cellFormula = "sum(${lastCellLetter}${startRow}:${lastCellLetter}${endRow})" + val cloneStyle = workbook.createCellStyle() + cloneStyle.cloneStyleFrom(boldFontWithBorderStyle) + cellStyle = cloneStyle.apply { + dataFormat = accountingStyle + } + } + + createCell(columnIndex).apply { + val lastCellLetter = CellReference.convertNumToColString(this.columnIndex) + cellFormula = "sum(${lastCellLetter}${startRow}:${lastCellLetter}${endRow})" + val cloneStyle = workbook.createCellStyle() + cloneStyle.cloneStyleFrom(boldFontWithBorderStyle) + cellStyle = cloneStyle.apply { + dataFormat = accountingStyle + } + } + } + + rowIndex += 2 + } + } + } + + conditionalFormattingNegative(sheet) 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 6c98fec..8f858ba 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 @@ -316,9 +316,9 @@ class ReportController( @Throws(ServletRequestBindingException::class, IOException::class) fun getCrossTeamChargeReport(@RequestBody @Valid request: CrossTeamChargeReportRequest): ResponseEntity { - val authorities = staffsService.currentAuthorities() ?: return ResponseEntity.noContent().build() - - if (authorities.stream().anyMatch { it.authority.equals("GENERATE_CROSS_TEAM_CHARGE_REPORT") }) { +// val authorities = staffsService.currentAuthorities() ?: return ResponseEntity.noContent().build() +// +// if (authorities.stream().anyMatch { it.authority.equals("G_CROSS_TEAM_CHARGE_REPORT") }) { // val crossProjects = staffAllocationRepository.findAll() // .filter { it.project?.teamLead?.team?.id != it.staff?.team?.id } // .map { it.project!! } @@ -342,8 +342,8 @@ class ReportController( .header("filename", "Cross Team Charge Report - " + LocalDate.now() + ".xlsx") .body(ByteArrayResource(reportResult)) - } else { - return ResponseEntity.noContent().build() - } +// } else { +// return ResponseEntity.noContent().build() +// } } } \ No newline at end of file diff --git a/src/main/resources/templates/report/Cross Team Charge Report.xlsx b/src/main/resources/templates/report/Cross Team Charge Report.xlsx index 1a4889b..54ec3cf 100644 Binary files a/src/main/resources/templates/report/Cross Team Charge Report.xlsx and b/src/main/resources/templates/report/Cross Team Charge Report.xlsx differ