From 26682b1e6ca9c8d64d22f688092675dce3aa65ee Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Tue, 16 Jul 2024 14:24:33 +0800 Subject: [PATCH] update cross team report, dashboard excel --- .../modules/data/service/DashboardService.kt | 2 +- .../modules/report/service/ReportService.kt | 249 +++++++++++------- .../modules/report/web/ReportController.kt | 19 +- .../modules/report/web/model/ReportRequest.kt | 1 + .../20240715_01_cyril/01_update_authority.sql | 36 +++ 5 files changed, 198 insertions(+), 109 deletions(-) create mode 100644 src/main/resources/db/changelog/changes/20240715_01_cyril/01_update_authority.sql 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 6c210c4..74f214a 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 @@ -2442,7 +2442,7 @@ open class DashboardService( } createCell(5).apply { - cellFormula = "IFERROR(IF(L${rowIndex}=0, 0, K${rowIndex}/J${rowIndex}),0)" + cellFormula = "IFERROR(IF(L${rowIndex}=0, 0,L${rowIndex}/K${rowIndex}),0)" cellStyle.apply { setFont(normalFont) dataFormat = accountingStyle 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 050303c..ffb9409 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 @@ -331,10 +331,11 @@ open class ReportService( timesheets: List, teams: List, grades: List, + teamId: String, ): ByteArray { // Generate the Excel report with query results val workbook: Workbook = - createCrossTeamChargeReport(month, timesheets, teams, grades, CROSS_TEAM_CHARGE_REPORT) + createCrossTeamChargeReport(month, timesheets, teams, grades, teamId, CROSS_TEAM_CHARGE_REPORT) // Write the workbook to a ByteArrayOutputStream val outputStream: ByteArrayOutputStream = ByteArrayOutputStream() @@ -2026,51 +2027,52 @@ open class ReportService( } open fun getProjectResourceOverconsumptionReport(args: Map): List> { - val sql = StringBuilder("with teamNormalConsumed as (" - + " SELECT" - + " t.projectId," - + " p.teamLead," - + " sum(t.normalConsumed) as normalConsumed," - + " sum(t.otConsumed) as otConsumed," - + " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) as totalConsumed," - + " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) * sal.hourlyRate as totalBudget" - + " from timesheet t" - + " left join project p on p.id = t.projectId" - + " left join staff s on s.id = p.teamLead" - + " left join salary sal on sal.salaryPoint = s.salaryId" - + " group by p.teamLead, t.projectId, sal.hourlyRate" - + " )" - + " SELECT" - + " p.id," - + " p.code," - + " p.name," - + " t.code," - + " concat(c.code, ' - ',c.name) as client," - + " COALESCE(concat(ss.code, ' - ', ss.name), 'N/A') as subsidiary," - + " p.expectedTotalFee * 0.8 as plannedBudget," - + " 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," - + " (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) as manhourConsumptionRate," - + " CASE" - + " when (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= :lowerLimit and (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) <= 1" - + " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= :lowerLimit and (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) <= 1" - + " then 'Potential Overconsumption'" - + " when (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= 1" - + " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= 1" - + " then 'Overconsumption'" - + " else 'Within Budget'" - + " END as status" - + " from project p" - + " left join staff s on s.id = p.teamLead" - + " left join team t on t.id = s.teamId" - + " left join customer c on c.id = p.customerId" - + " LEFT JOIN subsidiary ss on p.customerSubsidiaryId = ss.id" - + " LEFT JOIN salary sa ON s.salaryId = sa.salaryPoint" - + " left join teamNormalConsumed tns on tns.projectId = p.id" - + " WHERE p.deleted = false " - + " and p.status = 'On-going' " + val sql = StringBuilder( + "with teamNormalConsumed as (" + + " SELECT" + + " t.projectId," + + " p.teamLead," + + " sum(t.normalConsumed) as normalConsumed," + + " sum(t.otConsumed) as otConsumed," + + " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) as totalConsumed," + + " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) * sal.hourlyRate as totalBudget" + + " from timesheet t" + + " left join project p on p.id = t.projectId" + + " left join staff s on s.id = p.teamLead" + + " left join salary sal on sal.salaryPoint = s.salaryId" + + " group by p.teamLead, t.projectId, sal.hourlyRate" + + " )" + + " SELECT" + + " p.id," + + " p.code," + + " p.name," + + " t.code," + + " concat(c.code, ' - ',c.name) as client," + + " COALESCE(concat(ss.code, ' - ', ss.name), 'N/A') as subsidiary," + + " p.expectedTotalFee * 0.8 as plannedBudget," + + " 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," + + " (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) as manhourConsumptionRate," + + " CASE" + + " when (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= :lowerLimit and (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) <= 1" + + " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= :lowerLimit and (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) <= 1" + + " then 'Potential Overconsumption'" + + " when (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= 1" + + " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= 1" + + " then 'Overconsumption'" + + " else 'Within Budget'" + + " END as status" + + " from project p" + + " left join staff s on s.id = p.teamLead" + + " left join team t on t.id = s.teamId" + + " left join customer c on c.id = p.customerId" + + " LEFT JOIN subsidiary ss on p.customerSubsidiaryId = ss.id" + + " LEFT JOIN salary sa ON s.salaryId = sa.salaryPoint" + + " left join teamNormalConsumed tns on tns.projectId = p.id" + + " WHERE p.deleted = false " + + " and p.status = 'On-going' " ) if (args != null) { var statusFilter: String = "" @@ -3037,6 +3039,7 @@ open class ReportService( timesheets: List, teams: List, grades: List, + teamId: String, templatePath: String, ): Workbook { // please create a new function for each report template @@ -3044,7 +3047,7 @@ open class ReportService( val templateInputStream = resource.inputStream val workbook: Workbook = XSSFWorkbook(templateInputStream) - val sheet: Sheet = workbook.getSheetAt(0) + var sheet: Sheet = workbook.getSheetAt(0) // accounting style + comma style val accountingStyle = workbook.createDataFormat().getFormat("_(* #,##0.00_);_(* (#,##0.00);_(* \"-\"??_);_(@_)") @@ -3076,6 +3079,7 @@ open class ReportService( borderRight = BorderStyle.THIN } + // -------------------------- sheet 0 (By Grade) -------------------------- // var rowIndex = 1 // Assuming the location is in (1,2), which is the report date field var columnIndex = 2 val monthFormat = DateTimeFormatter.ofPattern("MMMM yyyy", Locale.ENGLISH) @@ -3088,9 +3092,12 @@ open class ReportService( // if (timesheets.isNotEmpty()) { val combinedTeamCodeColNumber = grades.size * 2 val sortedGrades = grades.sortedBy { it.id } - val sortedTeams = teams.sortedBy { it.id } + var sortedTeams = teams.sortedBy { it.id }.toMutableList() + if (teamId.lowercase() != "all") { + val teamIndex = sortedTeams.indexOfFirst { it.id == teamId.toLong() } + sortedTeams.add(0, sortedTeams.removeAt(teamIndex)) + } - logger.info(timesheets.filter { it.project?.teamLead?.team?.id != it.staff?.team?.id }.size) val groupedTimesheets = timesheets .filter { it.project?.teamLead?.team?.id != it.staff?.team?.id } .groupBy { timesheetEntry -> @@ -3200,75 +3207,84 @@ open class ReportService( val startRow = rowIndex var endRow = rowIndex sortedTeams.forEach { chargedTeam: Team -> - if (team.id != chargedTeam.id) { - endRow++ - sheet.createRow(rowIndex++).apply { - columnIndex = 0 - createCell(columnIndex++).apply { - setCellValue(chargedTeam.code) - val cloneStyle = workbook.createCellStyle() - cloneStyle.cloneStyleFrom(normalFontWithBorderStyle) - cellStyle = cloneStyle.apply { - alignment = HorizontalAlignment.CENTER + if (teamId.lowercase() == "all" || teamId.toLong() == chargedTeam.id || teamId.toLong() == team.id) { + if (team.id != chargedTeam.id) { + endRow++ + sheet.createRow(rowIndex++).apply { + columnIndex = 0 + createCell(columnIndex++).apply { + setCellValue(chargedTeam.code) + val cloneStyle = workbook.createCellStyle() + cloneStyle.cloneStyleFrom(normalFontWithBorderStyle) + cellStyle = cloneStyle.apply { + alignment = HorizontalAlignment.CENTER + } } - } - var totalSalary = 0.0 - sortedGrades.forEach { grade: Grade -> - createCell(columnIndex++).apply { - setCellValue( - groupedTimesheets[Triple( + var totalSalary = 0.0 + sortedGrades.forEach { grade: Grade -> + createCell(columnIndex++).apply { + setCellValue( + groupedTimesheets[Triple( + team.id, + chargedTeam.id, + grade.id + )]?.sumOf { it.getValue("manHour") } ?: 0.0) + + totalSalary += groupedTimesheets[Triple( team.id, chargedTeam.id, grade.id - )]?.sumOf { it.getValue("manHour") } ?: 0.0) + )]?.sumOf { it.getValue("salary") } ?: 0.0 - totalSalary += groupedTimesheets[Triple( - team.id, - chargedTeam.id, - grade.id - )]?.sumOf { it.getValue("salary") } ?: 0.0 + val cloneStyle = workbook.createCellStyle() + cloneStyle.cloneStyleFrom(normalFontWithBorderStyle) + cellStyle = cloneStyle.apply { + dataFormat = accountingStyle + } + } - val cloneStyle = workbook.createCellStyle() - cloneStyle.cloneStyleFrom(normalFontWithBorderStyle) - cellStyle = cloneStyle.apply { - dataFormat = accountingStyle + createCell(columnIndex++).apply { + setCellValue( + groupedTimesheets[Triple( + team.id, + chargedTeam.id, + grade.id + )]?.sumOf { it.getValue("salary") } ?: 0.0) + + val cloneStyle = workbook.createCellStyle() + cloneStyle.cloneStyleFrom(normalFontWithBorderStyle) + cellStyle = cloneStyle.apply { + dataFormat = accountingStyle + } } } createCell(columnIndex++).apply { - setCellValue( - groupedTimesheets[Triple( - team.id, - chargedTeam.id, - grade.id - )]?.sumOf { it.getValue("manHour") * it.getValue("salary") } ?: 0.0) +// val lastCellLetter = CellReference.convertNumToColString(this.columnIndex - 2) +// cellFormula = "sum(B${this.rowIndex + 1}, ${lastCellLetter}${this.rowIndex + 1})" + var cellFormulaStr = "sum(" + for (i in 1..(grades.size * 2) step 2) { + val cellLetter = CellReference.convertNumToColString(i) + cellFormulaStr += "${cellLetter}${this.rowIndex + 1}," + } + cellFormulaStr += ")" + cellFormula = cellFormulaStr val cloneStyle = workbook.createCellStyle() - cloneStyle.cloneStyleFrom(normalFontWithBorderStyle) + cloneStyle.cloneStyleFrom(boldFontWithBorderStyle) 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 + createCell(columnIndex).apply { + setCellValue(totalSalary) + val cloneStyle = workbook.createCellStyle() + cloneStyle.cloneStyleFrom(boldFontWithBorderStyle) + cellStyle = cloneStyle.apply { + dataFormat = accountingStyle + } } } } @@ -3315,6 +3331,39 @@ 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 +// } +// } +// } +// } 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 1e10b4b..3b51afa 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 @@ -317,23 +317,26 @@ class ReportController( val authorities = staffsService.currentAuthorities() ?: return ResponseEntity.noContent().build() - if (authorities.stream().anyMatch { it.authority.equals("GENERATE_REPORTS") }) { - val crossProjects = staffAllocationRepository.findAll() - .filter { it.project?.teamLead?.team?.id != it.staff?.team?.id } - .map { it.project!! } - .distinct() + if (authorities.stream().anyMatch { it.authority.equals("GENERATE_CROSS_TEAM_CHARGE_REPORT") }) { +// val crossProjects = staffAllocationRepository.findAll() +// .filter { it.project?.teamLead?.team?.id != it.staff?.team?.id } +// .map { it.project!! } +// .distinct() val reportMonth = YearMonth.parse(request.month, DateTimeFormatter.ofPattern("yyyy-MM")) val convertReportMonth = YearMonth.of(reportMonth.year, reportMonth.month) val startDate = convertReportMonth.atDay(1) val endDate = convertReportMonth.atEndOfMonth() - val timesheets = timesheetRepository.findAllByProjectIn(crossProjects) - .filter { it.recordDate != null && +// val timesheets = timesheetRepository.findAllByProjectIn(crossProjects) +// .filter { it.recordDate != null && +// (it.recordDate!!.isEqual(startDate) || it.recordDate!!.isEqual(endDate) || (it.recordDate!!.isAfter(startDate) && it.recordDate!!.isBefore(endDate)))} + val timesheets = timesheetRepository.findAll() + .filter { it.deleted == false && it.recordDate != null && it.staff != null && it.staff!!.team != null && it.project != null && (it.recordDate!!.isEqual(startDate) || it.recordDate!!.isEqual(endDate) || (it.recordDate!!.isAfter(startDate) && it.recordDate!!.isBefore(endDate)))} val teams = teamRepository.findAll().filter { it.deleted == false } val grades = gradeRepository.findAll().filter { it.deleted == false } - val reportResult: ByteArray = excelReportService.generateCrossTeamChargeReport(request.month, timesheets, teams, grades) + val reportResult: ByteArray = excelReportService.generateCrossTeamChargeReport(request.month, timesheets, teams, grades, request.teamId) return ResponseEntity.ok() .header("filename", "Cross Team Charge Report - " + LocalDate.now() + ".xlsx") .body(ByteArrayResource(reportResult)) 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 b66e46a..cfa302d 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 @@ -62,4 +62,5 @@ data class ProjectCompletionReport ( data class CrossTeamChargeReportRequest ( val month: String, + val teamId: String ) \ No newline at end of file diff --git a/src/main/resources/db/changelog/changes/20240715_01_cyril/01_update_authority.sql b/src/main/resources/db/changelog/changes/20240715_01_cyril/01_update_authority.sql new file mode 100644 index 0000000..a458fdc --- /dev/null +++ b/src/main/resources/db/changelog/changes/20240715_01_cyril/01_update_authority.sql @@ -0,0 +1,36 @@ +-- liquibase formatted sql +-- changeset cyril:authority, user_authority + +UPDATE tsmsdb.authority +SET authority='GENERATE_LATE_START_REPORT',name='Generate Late Start Report' +WHERE id=17; + +INSERT INTO authority (authority,name) +VALUES ('GENERATE_PROJECT_POTENTIAL_DELAY_REPORT','Generate Project Potential Delay Report'); +INSERT INTO authority (authority,name) +VALUES ('GENERATE_RESOURCE_OVERCONSUMPTION_REPORT','Generate Resource Overconsumption Report'); +INSERT INTO authority (authority,name) +VALUES ('GENERATE_COST_ANT_EXPENSE_REPORT','Generate Cost And Expense Report'); +INSERT INTO authority (authority,name) +VALUES ('GENERATE_PROJECT_COMPLETION_REPORT','Generate Project Completion Report'); +INSERT INTO authority (authority,name) +VALUES ('GENERATE_PROJECT_P&L_REPORT','Generate Project P&L Report'); +INSERT INTO authority (authority,name) +VALUES ('GENERATE_FINANCIAL_STATUS_REPORT','Generate Financial Status Report'); +INSERT INTO authority (authority,name) +VALUES ('GENERATE_PROJECT_CASH_FLOW_REPORT','Generate Project Cash Flow Report'); +INSERT INTO authority (authority,name) +VALUES ('GENERATE_STAFF_MONTHLY_WORK_HOURS_ANALYSIS_REPORT','Generate Staff Monthly Work Hours Analysis Report'); +INSERT INTO authority (authority,name) +VALUES ('GENERATE_CROSS_TEAM_CHARGE_REPORT','Generate Cross Team Charge Report'); + +INSERT INTO `user_authority` VALUES + (1,44), + (1,45), + (1,46), + (1,47), + (1,48), + (1,49), + (1,50), + (1,51), + (1,52);