diff --git a/src/main/java/com/ffii/tsms/modules/data/entity/GradeLogRepository.java b/src/main/java/com/ffii/tsms/modules/data/entity/GradeLogRepository.java index 5daad9b..4ae08ac 100644 --- a/src/main/java/com/ffii/tsms/modules/data/entity/GradeLogRepository.java +++ b/src/main/java/com/ffii/tsms/modules/data/entity/GradeLogRepository.java @@ -3,9 +3,13 @@ package com.ffii.tsms.modules.data.entity; import com.ffii.core.support.AbstractRepository; import com.ffii.tsms.modules.data.entity.projections.GradeLogInfo; +import java.time.LocalDate; import java.util.List; public interface GradeLogRepository extends AbstractRepository { List findGradeLogInfoByStaffIdAndDeletedFalseOrderByCreatedDesc(Long staffId); GradeLog findFirstByStaffIdAndDeletedFalseOrderByCreatedDesc(Long staffId); + + List findByDeletedFalseAndFromBeforeAndToIsNullOrToAfter(LocalDate before, LocalDate after); +// fun findByDeletedFalseAndRecordDateBetweenOrderByRecordDate(start: LocalDate, end: LocalDate): List } 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 2e7dc64..b429a19 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 @@ -2217,11 +2217,19 @@ open class DashboardService( // + " ) as expenditure on 1 = 1" // + " where p.id = :projectId" // + " group by expenditure.expenditure" - " select" + "with pe_cte as (" + + " select " + + " pe.projectId " + + " ,sum(pe.amount) as expense " + + " from project_expense pe " + + " where pe.deleted = false " + + " group by pe.projectId " + + " ) " + + " select" + " concat(p.code,'-',p.name) as projectCodeAndName," + " p.expectedTotalFee as totalFee," + " (p.expectedTotalFee - ifnull(p.subContractFee, 0)) * 0.8 as totalBudget," - + " coalesce (expenditure.expenditure,0) as expenditure," + + " coalesce (expenditure.expenditure,0) + coalesce(pc.expense, 0) as expenditure, " + " ((p.expectedTotalFee - ifnull(p.subContractFee, 0)) * 0.8) - coalesce (expenditure.expenditure,0) as remainingBudget," + " case" + " when p.expectedTotalFee - coalesce (expenditure.expenditure,0) >= 0 then 'Within Budget'" @@ -2230,7 +2238,9 @@ open class DashboardService( + " p.totalManhour as plannedResources," + " sum(ifnull(t.normalConsumed, 0) + ifnull(t.otConsumed, 0)) as resourcesSpent," + " p.totalManhour - sum(ifnull(t.normalConsumed, 0) + ifnull(t.otConsumed, 0)) as remainingResources" + + " , pc.expense" + " from project p" + + " left join pe_cte pc on pc.projectId = p.id " + " left join project_task pt on p.id = pt.project_id" + " left join timesheet t on pt.id = t.projectTaskId" + " left join(" 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 650c151..112b689 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 @@ -33,17 +33,23 @@ import java.util.* import java.awt.Color import java.math.RoundingMode import java.time.Year +import javax.swing.plaf.synth.Region import kotlin.collections.ArrayList data class DayInfo(val date: String?, val weekday: String?) - +data class TsData(var manhour: Double, var cost: Double) +data class InOut(var In: TsData, var Out: TsData) @Service open class ReportService( private val jdbcDao: JdbcDao, private val projectRepository: ProjectRepository, - private val salaryEffectiveService: SalaryEffectiveService, private val salaryRepository: SalaryRepository, private val timesheetRepository: TimesheetRepository + private val salaryEffectiveService: SalaryEffectiveService, + private val salaryEffectiveRepository: SalaryEffectiveRepository, + private val salaryRepository: SalaryRepository, + private val timesheetRepository: TimesheetRepository, + private val teamLogRepository: TeamLogRepository ) { private val logger: Log = LogFactory.getLog(javaClass) private val DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy/MM/dd") @@ -63,6 +69,45 @@ open class ReportService( private val COMPLETION_PROJECT = "templates/report/AR05_Project Completion Report.xlsx" private val CROSS_TEAM_CHARGE_REPORT = "templates/report/Cross Team Charge Report.xlsx" + private fun cellBorderArgs(top: Int, bottom: Int, left: Int, right: Int): MutableMap { + var cellBorderArgs = mutableMapOf() + when (top) { + 1 -> cellBorderArgs.put(CellUtil.BORDER_TOP, BorderStyle.THIN) + 2 -> cellBorderArgs.put(CellUtil.BORDER_TOP, BorderStyle.THICK) + } + when (bottom) { + 1 -> cellBorderArgs.put(CellUtil.BORDER_BOTTOM, BorderStyle.THIN) + 2 -> cellBorderArgs.put(CellUtil.BORDER_BOTTOM, BorderStyle.THICK) + } + when (left) { + 1 -> cellBorderArgs.put(CellUtil.BORDER_LEFT, BorderStyle.THIN) + 2 -> cellBorderArgs.put(CellUtil.BORDER_LEFT, BorderStyle.THICK) + } + when (right) { + 1 -> cellBorderArgs.put(CellUtil.BORDER_RIGHT, BorderStyle.THIN) + 2 -> cellBorderArgs.put(CellUtil.BORDER_RIGHT, BorderStyle.THICK) + } + return cellBorderArgs + } + private fun setRegionBorders(top: Int, bottom: Int, left: Int, right: Int, region: CellRangeAddress, sheet: Sheet) { + when (top) { + 1 -> RegionUtil.setBorderTop(BorderStyle.THIN, region, sheet) + 2 -> RegionUtil.setBorderTop(BorderStyle.THICK, region, sheet) + } + when (bottom) { + 1 -> RegionUtil.setBorderBottom(BorderStyle.THIN, region, sheet) + 2 -> RegionUtil.setBorderBottom(BorderStyle.THICK, region, sheet) + } + when (left) { + 1 -> RegionUtil.setBorderLeft(BorderStyle.THIN, region, sheet) + 2 -> RegionUtil.setBorderLeft(BorderStyle.THICK, region, sheet) + } + when (right) { + 1 -> RegionUtil.setBorderRight(BorderStyle.THIN, region, sheet) + 2 -> RegionUtil.setBorderRight(BorderStyle.THICK, region, sheet) + } + } + private val chargeFee = 1.15 private fun conditionalFormattingNegative(sheet: Sheet) { // Create a conditional formatting rule @@ -330,6 +375,7 @@ open class ReportService( grades: List, monthlyStaffSalaryEffective: List, teamId: String, + gradeLog: List, ): ByteArray { // Generate the Excel report with query results val workbook: Workbook = @@ -340,6 +386,7 @@ open class ReportService( grades, monthlyStaffSalaryEffective, teamId, + gradeLog, CROSS_TEAM_CHARGE_REPORT ) @@ -3547,6 +3594,342 @@ open class ReportService( return jdbcDao.queryForList(sql.toString(), args) } + private fun generateTeamBlock( + teams: List, + sortedTeams: MutableList, + grades: List, + timesheets: List, + gradeLog: List, + salaryEffective: List, + ): MutableMap> { +// val teamlog = teamLogRepository.findAll().filter { it.deleted == false } + val gradeMap: MutableMap = mutableMapOf() + var teamsMap: MutableMap> = mutableMapOf() + grades.forEach { + val key = it.code + if (key !in gradeMap) { + gradeMap[key] = InOut( + TsData(0.0, 0.0), + TsData(0.0, 0.0) + ) + } + } + teams.forEach { team -> + val key = team.code + if (key !in teamsMap) { + // Create a new map for each team with copies of the entries from gradeMap + teamsMap[key] = gradeMap.mapValues { (_, value) -> + InOut( + TsData(value.In.manhour, value.In.cost), + TsData(value.Out.manhour, value.Out.cost) + ) + }.toMutableMap() + } + } + println("teamsMap") + println(gradeMap) +// teamsMap["TW"]!!["1"]!!.In.manhour += 1000 + println(teamsMap) + sortedTeams.forEach { team -> + val currentTeam = team.code + val _timesheets = timesheets.filter { ts -> +// for team log +// val thisTeam = teamlog.filter { +// it.from.isBefore(ts.recordDate) +// && it.to != null +// && it.staff.id == ts.staff!!.id +// }.maxByOrNull { it.from } + ts.project!!.teamLead!!.team.code == currentTeam // check isChargingTeam + && + ts.project!!.teamLead!!.team.code != ts.staff!!.team.code // check isCrossTeam +// ts.project!!.teamLead!!.team.id != thisTeam!!.team.id // for team log + } + _timesheets.forEach {ts -> + // this team charging others + // get the grade and salary data of the record + val _grade = gradeLog.find { it.staff.id == ts.staff!!.id } + val gradeCode = _grade!!.grade.code + val otMultiplier = 1 + val thisSE = salaryEffective.filter { + it.staff.id == ts.staff!!.id + && + it.date.isBefore(ts.recordDate) + }.maxByOrNull { it.date } + val normalHour = ts.normalConsumed ?: 0.0 + val otHour = ts.otConsumed ?: 0.0 + val normalCost = normalHour.times(thisSE!!.salary.hourlyRate.toDouble()) + val otCost = otHour.times(thisSE.salary.hourlyRate.toDouble()).times(otMultiplier) + //assigning data + val projectTeam = ts.project!!.teamLead!!.team.code + val staffTeam = ts.staff!!.team.code + // write in + println("putting in") + var tsInData = teamsMap[projectTeam]!![gradeCode]!!.In + println(tsInData) + tsInData.manhour += normalHour + otHour + tsInData.cost += normalCost + otCost + // write out + println("putting out") + val tsOutData = teamsMap[staffTeam]!!.get(gradeCode)!!.Out + tsOutData.manhour += normalHour + otHour + tsOutData.cost += normalCost + otCost + } + } + println("all team - gradeMap") + println(teamsMap) + return teamsMap + } + + + private fun alignTopCenter(tempCell: Cell) { + CellUtil.setVerticalAlignment(tempCell, VerticalAlignment.TOP) + CellUtil.setAlignment(tempCell, HorizontalAlignment.CENTER) + } + private fun createCrossTeamForm( + workbook: Workbook, + sheet: Sheet, + _rowIndex: Int, + _columnIndex: Int, + teamsMap: MutableMap> + ) { + var rowIndex = _rowIndex + var columnIndex = _columnIndex + var tempRow: Row + var tempCell: Cell + + val accountingStyle = workbook.createDataFormat().getFormat("_(* #,##0.00_);_(* (#,##0.00);_(* \"-\"??_);_(@_)") + + fun dataFormatArgs(accountingStyle: Short): MutableMap { + val dataFormatArgs = mutableMapOf( + CellUtil.DATA_FORMAT to accountingStyle + ) + return dataFormatArgs + } + + fun fontArgs(isBold: Boolean): MutableMap{ + val font = sheet.workbook.createFont() + font.bold = isBold + font.fontName = "Times New Roman" + val fontArgs = mutableMapOf( + CellUtil.FONT to font.index, + CellUtil.WRAP_TEXT to true, + ) + return fontArgs + } + + val InHourLetter = CellReference.convertNumToColString(3) // col D + val InCostLetter = CellReference.convertNumToColString(4) // col E + val OutHourLetter = CellReference.convertNumToColString(5) // col F + val OutCostLetter = CellReference.convertNumToColString(6) // col G + val NetHourLetter = CellReference.convertNumToColString(7) // col G + val NetCostLetter = CellReference.convertNumToColString(8) // col G + val subTotalRowIndexList = mutableListOf() + + teamsMap.forEach{ (teamCode, gradeMap) -> + columnIndex = 0 + tempRow = getOrCreateRow(sheet, rowIndex) + tempCell = getOrCreateCell(tempRow, columnIndex) + tempCell.setCellValue(teamCode) + alignTopCenter(tempCell) + val teamCodeRegion = CellRangeAddress(rowIndex, rowIndex + 5,0,0) + sheet.addMergedRegion(teamCodeRegion) + setRegionBorders(2,2,1,1, teamCodeRegion, sheet) + CellUtil.setCellStyleProperties(tempCell, fontArgs(false)) + columnIndex++ + + val gradeRegion = CellRangeAddress(rowIndex, rowIndex + 4,1,1) + tempCell = getOrCreateCell(tempRow, columnIndex) + tempCell.setCellValue("Grade") + setRegionBorders(1,1,1,1, gradeRegion, sheet) + CellUtil.setCellStyleProperties(tempCell, fontArgs(true)) + alignTopCenter(tempCell) + sheet.addMergedRegion(gradeRegion) + columnIndex++ + + gradeMap.forEach{ (gradeCode, inOut) -> + tempRow = getOrCreateRow(sheet, rowIndex) + tempCell = getOrCreateCell(tempRow, columnIndex).apply { + setCellValue(gradeCode) + alignTopCenter(this) + } + CellUtil.setCellStyleProperties(tempCell, cellBorderArgs(1,1,1,1)+fontArgs(false)) + for (i in 1..6) { // horizontal loop: payment In & payment Out & Net Amount + tempCell = getOrCreateCell(tempRow, columnIndex + i).apply { + when (i) { + 1 -> setCellValue(inOut.In.manhour) + 2 -> setCellValue(inOut.In.cost) + 3 -> setCellValue(inOut.Out.manhour) + 4 -> setCellValue(inOut.Out.cost) + 5 -> cellFormula = "${InHourLetter}${rowIndex+1}-${OutHourLetter}${rowIndex+1}" + 6 -> cellFormula = "${InCostLetter}${rowIndex+1}-${OutCostLetter}${rowIndex+1}" + } + CellUtil.setCellStyleProperties(this, + cellBorderArgs(1,1,1,1) + + fontArgs(false) + + dataFormatArgs(accountingStyle) + ) + } + } // end loop: payment In & payment Out & Net Amount + rowIndex++ + } + // write subtotal + columnIndex = 1 + tempRow = getOrCreateRow(sheet, rowIndex) + tempCell = getOrCreateCell(tempRow, columnIndex).apply { + setCellValue("Sub-total:") + CellUtil.setCellStyleProperties(this, + cellBorderArgs(1,1,1,1) + + fontArgs(false)) + alignTopCenter(this) + } + subTotalRowIndexList.add(rowIndex) + // InHour subtotal + columnIndex = 3 + tempCell = getOrCreateCell(tempRow, columnIndex).apply { + cellFormula = "SUM(${InHourLetter}${rowIndex-4}:${InHourLetter}${rowIndex})" + CellUtil.setCellStyleProperties(this, + cellBorderArgs(1,1,1,1) + + fontArgs(false) + + dataFormatArgs(accountingStyle) + ) + } + // InCost subtotal + columnIndex = 4 + tempCell = getOrCreateCell(tempRow, columnIndex).apply { + cellFormula = "SUM(${InCostLetter}${rowIndex-4}:${InCostLetter}${rowIndex})" + CellUtil.setCellStyleProperties(this, + cellBorderArgs(1,1,1,1) + + fontArgs(false) + + dataFormatArgs(accountingStyle) + ) + } + // OutHour subtotal + columnIndex = 5 + tempCell = getOrCreateCell(tempRow, columnIndex).apply { + cellFormula = "SUM(${OutHourLetter}${rowIndex-4}:${OutHourLetter}${rowIndex})" + CellUtil.setCellStyleProperties(this, + cellBorderArgs(1,1,1,1) + + fontArgs(false) + + dataFormatArgs(accountingStyle) + ) + } + // OutCost subtotal + columnIndex = 6 + tempCell = getOrCreateCell(tempRow, columnIndex).apply { + cellFormula = "SUM(${OutCostLetter}${rowIndex-4}:${OutCostLetter}${rowIndex})" + CellUtil.setCellStyleProperties(this, + cellBorderArgs(1,1,1,1) + + fontArgs(false) + + dataFormatArgs(accountingStyle) + ) + } + // NetHour subtotal + columnIndex = 7 + tempCell = getOrCreateCell(tempRow, columnIndex).apply { + cellFormula = "SUM(${NetHourLetter}${rowIndex-4}:${NetHourLetter}${rowIndex})" + CellUtil.setCellStyleProperties(this, cellBorderArgs(1,1,1,1) + + fontArgs(false) + + dataFormatArgs(accountingStyle) + ) + } + // NetCost subtotal + columnIndex = 8 + tempCell = getOrCreateCell(tempRow, columnIndex).apply { + cellFormula = "SUM(${NetCostLetter}${rowIndex-4}:${NetCostLetter}${rowIndex})" + CellUtil.setCellStyleProperties(this, cellBorderArgs(1,1,1,1) + + fontArgs(false) + + dataFormatArgs(accountingStyle) + ) + } + // merge at last + val subtotalRegion = CellRangeAddress(rowIndex, rowIndex,1,2) + sheet.addMergedRegion(subtotalRegion) + val subtotalRowRegion = CellRangeAddress(rowIndex, rowIndex,0,8) + setRegionBorders(1,2,1,1, subtotalRowRegion, sheet) + rowIndex++ + } + // rowIndex == 47 + columnIndex = 1 + tempRow = getOrCreateRow(sheet, rowIndex) + tempCell = getOrCreateCell(tempRow, columnIndex).apply { + setCellValue("Total:") + CellUtil.setCellStyleProperties(this, + cellBorderArgs(1,1,1,1) + + fontArgs(false) + ) + alignTopCenter(this) + } + val totalRegion = CellRangeAddress(rowIndex, rowIndex,1,2) + sheet.addMergedRegion(totalRegion) + for (i in 3..8) { + columnIndex = i + val subtotalList = mutableListOf() + val currColumnLetter = CellReference.convertNumToColString(i) + subTotalRowIndexList.forEach{ + subtotalList.add("${currColumnLetter}${it+1}") + } + val subtotalFormula = subtotalList.joinToString("+") + tempCell = getOrCreateCell(tempRow, columnIndex).apply { + cellFormula = subtotalFormula + CellUtil.setCellStyleProperties(this, cellBorderArgs(1,1,1,1) + + fontArgs(false) + + dataFormatArgs(accountingStyle) + ) + } + } + val subtotalRowRegion = CellRangeAddress(rowIndex, rowIndex,0,8) + setRegionBorders(1,2,1,1, subtotalRowRegion, sheet) + } + + private fun createThirdSheetTeamChargeReport( + month: String, + workbook: Workbook, + timesheets: List, + teams: List, + grades: List, + teamId: String, + gradeLog: List, + isTeamLead: Boolean, + ) { + val salaryEffective = salaryEffectiveRepository.findAll() + var sheet: Sheet = workbook.getSheetAt(2) + val accountingStyle = workbook.createDataFormat().getFormat("_(* #,##0.00_);_(* (#,##0.00);_(* \"-\"??_);_(@_)") + val rowIndex = 1 // Assuming the location is in (1,2), which is the report date field + val columnIndex = 2 + val monthFormat = DateTimeFormatter.ofPattern("MMMM yyyy", Locale.ENGLISH) + val reportMonth = YearMonth.parse(month, DateTimeFormatter.ofPattern("yyyy-MM")) + val convertReportMonth = YearMonth.of(reportMonth.year, reportMonth.month).format(monthFormat) + sheet.getRow(rowIndex).getCell(columnIndex).apply { + setCellValue(convertReportMonth) + } + var sortedTeams = teams.sortedBy { it.id }.toMutableList() + if (teamId.lowercase() != "all") { +// sortedTeams = teams.sortedWith(compareBy { if (it.id == teamId.toLong()) 0 else 1 }).toMutableList() + sortedTeams = mutableListOf(sortedTeams.find { teamId.toLong() == it.id }!!) + } + //// generate info map ///// + val teamsInOutMap: MutableMap> = generateTeamBlock( + teams, + sortedTeams, + grades, + timesheets, + gradeLog, + salaryEffective, + ) + ///// end of function //// + ///// create the form //// + createCrossTeamForm( + workbook, + sheet, + 5, + 0, + teamsInOutMap + ) + // autosize + for (i in 3..8) { + sheet.setColumnWidth(i, 20 * 256) + } + } @Throws(IOException::class) private fun createCrossTeamChargeReport( month: String, @@ -3555,6 +3938,7 @@ open class ReportService( grades: List, monthlyStaffSalaryEffective: List, teamId: String, + gradeLog: List, templatePath: String, ): Workbook { // please create a new function for each report template @@ -4119,10 +4503,19 @@ open class ReportService( } } } - + val isTeamLead = false conditionalFormattingNegative(sheet) conditionalFormattingPositive(sheet) - + createThirdSheetTeamChargeReport( + month, + workbook, + timesheets, + teams, + grades, + teamId, + gradeLog, + isTeamLead, + ) return workbook } // Use to Calculate cummunlative expenditure 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 62d0190..9d5ccc2 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 @@ -63,6 +63,7 @@ class ReportController( private val invoiceService: InvoiceService, private val gradeAllocationRepository: GradeAllocationRepository, private val subsidiaryRepository: SubsidiaryRepository, private val staffAllocationRepository: StaffAllocationRepository, private val gradeRepository: GradeRepository, + private val gradeLogRepository: GradeLogRepository, private val salaryEffectiveService: SalaryEffectiveService, private val projectExpenseRepository: ProjectExpenseRepository, ) { @@ -342,8 +343,9 @@ class ReportController( val teams = teamRepository.findAll().filter { it.deleted == false } val grades = gradeRepository.findAll().filter { it.deleted == false } val monthlyStaffSalaryEffective = salaryEffectiveService.getMonthlyStaffSalaryData(startDate, endDate) - - val reportResult: ByteArray = excelReportService.generateCrossTeamChargeReport(request.month, timesheets, teams, grades, monthlyStaffSalaryEffective, request.teamId) + val gradeLog = gradeLogRepository.findByDeletedFalseAndFromBeforeAndToIsNullOrToAfter(startDate, endDate) + print(gradeLog) + val reportResult: ByteArray = excelReportService.generateCrossTeamChargeReport(request.month, timesheets, teams, grades, monthlyStaffSalaryEffective, request.teamId, gradeLog) return ResponseEntity.ok() .header("filename", "Cross Team Charge Report - " + LocalDate.now() + ".xlsx") .body(ByteArrayResource(reportResult)) 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 02a17c8..cc17ad6 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