| @@ -2,6 +2,7 @@ package com.ffii.tsms.modules.report.service | |||
| import com.ffii.core.support.JdbcDao | |||
| import com.ffii.tsms.modules.data.entity.* | |||
| import com.ffii.tsms.modules.data.entity.projections.SalaryEffectiveInfo | |||
| import com.ffii.tsms.modules.data.service.SalaryEffectiveService | |||
| import com.ffii.tsms.modules.project.entity.Invoice | |||
| import com.ffii.tsms.modules.project.entity.Milestone | |||
| @@ -32,6 +33,7 @@ import java.time.format.DateTimeParseException | |||
| import java.time.temporal.ChronoUnit | |||
| import java.util.* | |||
| import java.awt.Color | |||
| import java.math.RoundingMode | |||
| import java.time.Year | |||
| import kotlin.collections.ArrayList | |||
| @@ -123,56 +125,24 @@ open class ReportService( | |||
| fun genFinancialStatusReport(teamLeadId: Long): ByteArray { | |||
| val financialStatus: List<Map<String, Any>> = getFinancialStatus(teamLeadId) | |||
| val otFactor = BigDecimal(1) | |||
| val tempList = mutableListOf<Map<String, Any>>() | |||
| 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 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"), | |||
| "subsidiary" to item.getValue("subsidiary"), | |||
| "teamLead" to item.getValue("teamLead"), | |||
| "planStart" to item.getValue("planStart"), | |||
| "planEnd" to item.getValue("planEnd"), | |||
| "expectedTotalFee" to item.getValue("expectedTotalFee"), | |||
| "subContractFee" to item.getValue("subContractFee"), | |||
| "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")) }!! | |||
| // Update the existing Map with the new manHourRate and manOtHourRate values | |||
| tempList[tempList.indexOf(existingMap)] = existingMap.toMutableMap().apply { | |||
| put("normalConsumed", (get("normalConsumed") as BigDecimal).add(manHourRate)) | |||
| put("otConsumed", (get("otConsumed") as BigDecimal).add(manOtHourRate)) | |||
| val manhoursSpent = getManHoursSpentByTeam(teamLeadId) | |||
| val salaryEffectiveMap = getSalaryEffectiveByTeamLead(teamLeadId) | |||
| val updatedTimesheetData = updateTimesheetDataWithEffectiveSalary(manhoursSpent, salaryEffectiveMap) | |||
| val projectsExpenditure = calculateProjectExpenditures(updatedTimesheetData) | |||
| val updatedList = financialStatus.map { item -> | |||
| val code = item["code"] as? String | |||
| val expenditure = projectsExpenditure[code] | |||
| item.toMutableMap().apply { | |||
| if (code != null && expenditure != null) { | |||
| this["totalCumulativeExpenditure"] = expenditure | |||
| }else{ | |||
| this["totalCumulativeExpenditure"] = BigDecimal.ZERO | |||
| } | |||
| } | |||
| } | |||
| // println("tempList---------------------- $tempList") | |||
| val workbook: Workbook = createFinancialStatusReport(FINANCIAL_STATUS_REPORT, tempList, teamLeadId) | |||
| val workbook: Workbook = createFinancialStatusReport(FINANCIAL_STATUS_REPORT, updatedList, teamLeadId) | |||
| val outputStream: ByteArrayOutputStream = ByteArrayOutputStream() | |||
| workbook.write(outputStream) | |||
| @@ -474,11 +444,12 @@ open class ReportService( | |||
| } | |||
| val cumExpenditureCell = row.createCell(9) | |||
| 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) | |||
| // 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) | |||
| val totalCumulativeExpenditure = item["totalCumulativeExpenditure"]?.let { it as BigDecimal } ?: BigDecimal(0) | |||
| cumExpenditureCell.apply { | |||
| setCellValue(cumExpenditure.toDouble()) | |||
| setCellValue(totalCumulativeExpenditure.toDouble()) | |||
| cellStyle.dataFormat = accountingStyle | |||
| } | |||
| @@ -512,7 +483,7 @@ open class ReportService( | |||
| val projectedCpiCell = row.createCell(14) | |||
| projectedCpiCell.apply { | |||
| cellFormula = "(H${rowNum}/J${rowNum})" | |||
| cellFormula = "IF(J${rowNum} = 0, 0, H${rowNum}/J${rowNum})" | |||
| cellStyle = boldFontCellStyle | |||
| } | |||
| @@ -1925,26 +1896,27 @@ open class ReportService( | |||
| open fun getFinancialStatus(teamLeadId: Long?): List<Map<String, Any>> { | |||
| 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 (" | |||
| // " 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" | |||
| // + " )," | |||
| " With 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, IFNULL(s2.name, \"N/A\") as subsidiary, concat(t.code, \' - \', t.name) as teamLead, p.planStart , p.planEnd , p.expectedTotalFee, ifnull(p.subContractFee, 0) as subContractFee, " | |||
| + " 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" | |||
| // + " 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" | |||
| + " ,0 as totalCumulativeExpenditure " | |||
| + " from project p" | |||
| + " left join cte_timesheet cte_ts on p.code = cte_ts.code" | |||
| // + " left join cte_timesheet cte_ts on p.code = cte_ts.code" | |||
| + " left join customer c on c.id = p.customerId" | |||
| + " left join customer_subsidiary cs on cs.id = p.customerSubsidiaryId" | |||
| + " left join subsidiary s2 on s2.id = cs.subsidiaryId " | |||
| @@ -2293,7 +2265,7 @@ open class ReportService( | |||
| "code" to projectsCode["code"], | |||
| "description" to projectsCode["description"] | |||
| ) | |||
| val financialYears = getHalfYearFinancialPeriods(queryStartMonth, queryEndMonth, 10) | |||
| val financialYears = getHalfYearFinancialPeriods(queryStartMonth, queryEndMonth, 1) | |||
| val staffInfoList = mutableListOf<Map<String, Any>>() | |||
| @@ -2302,7 +2274,7 @@ open class ReportService( | |||
| for (item in manHoursSpent) { | |||
| updateInfo(info, item) | |||
| val hourlyRate = getSalaryForMonth(item.getValue("recordDate") as String, item.getValue("staffId") as String, staffSalaryLists, queryStartMonth, queryEndMonth) ?: (item.getValue("hourlyRate") as BigDecimal).toDouble() | |||
| val hourlyRate = getSalaryForMonth(item.getValue("recordDate") as String, item.getValue("staffId") as String, staffSalaryLists) ?: (item.getValue("hourlyRate") as BigDecimal).toDouble() | |||
| if (!staffInfoList.any { it["staffId"] == item["staffId"] && it["name"] == item["name"] }) { | |||
| @@ -2374,37 +2346,24 @@ open class ReportService( | |||
| return tempList | |||
| } | |||
| fun getSalaryForMonth(recordDate: String, staffId: String, staffSalaryLists: List<SalaryEffectiveService.StaffSalaryData>, start: YearMonth, end: YearMonth): Double? { | |||
| fun getSalaryForMonth(recordDate: String, staffId: String, staffSalaryLists: List<SalaryEffectiveService.StaffSalaryData>): Double? { | |||
| val formatter = DateTimeFormatter.ofPattern("yyyy-MM") | |||
| val monthDate = YearMonth.parse(recordDate, formatter) | |||
| val staffSalaryData = staffSalaryLists.find { it.staffId == staffId }?.salaryData ?: return null | |||
| return findSalaryForMonth(staffSalaryData, monthDate, start, end) | |||
| return findSalaryForMonth(staffSalaryData, monthDate) | |||
| } | |||
| private fun findSalaryForMonth(salaryDataList: List<SalaryEffectiveService.SalaryData>, targetMonth: YearMonth, start: YearMonth, end: YearMonth): Double? { | |||
| if (salaryDataList.isEmpty()) return null | |||
| val periodStartMonth = 10 | |||
| val financialPeriods = getHalfYearFinancialPeriods(start, end, periodStartMonth) | |||
| // First, try to find a salary in the current financial period | |||
| val currentFinancialPeriod = financialPeriods.find { targetMonth in it.start..it.end } | |||
| val currentSalary = currentFinancialPeriod?.let { period -> | |||
| salaryDataList.find { salaryData -> | |||
| val salaryYearMonth = YearMonth.from(salaryData.financialYear) | |||
| salaryYearMonth in period.start..period.end | |||
| }?.hourlyRate?.toDouble() | |||
| } | |||
| if (currentSalary != null) { | |||
| return currentSalary | |||
| } | |||
| // If not found, find the most recent previous salary | |||
| return findPreviousValue(salaryDataList, targetMonth) { it.hourlyRate.toDouble() } | |||
| private fun findSalaryForMonth(salaryDataList: List<SalaryEffectiveService.SalaryData>, targetMonth: YearMonth): Double? { | |||
| return salaryDataList | |||
| .filter { salaryData -> | |||
| val effectiveDate = YearMonth.from(salaryData.financialYear) | |||
| effectiveDate <= targetMonth | |||
| } | |||
| .maxByOrNull { it.financialYear } | |||
| ?.hourlyRate | |||
| ?.toDouble() | |||
| } | |||
| fun updateStaffFinancialYears( | |||
| @@ -2430,7 +2389,7 @@ open class ReportService( | |||
| } | |||
| fun updateFinancialYear(financialYear: FinancialYear, salaryDataList: List<SalaryEffectiveService.SalaryData>?, staffInfo: Map<String, Any>): FinancialYear { | |||
| println("====================== staffInfo: $staffInfo ===============================") | |||
| // println("====================== staffInfo: $staffInfo ===============================") | |||
| if(salaryDataList == null){ | |||
| return financialYear.copy( | |||
| @@ -2444,7 +2403,7 @@ open class ReportService( | |||
| salaryYearMonth >= financialYear.start && salaryYearMonth <= financialYear.end | |||
| } | |||
| println("====================== relevantSalaryData: $relevantSalaryData ===============================") | |||
| // println("====================== relevantSalaryData: $relevantSalaryData ===============================") | |||
| if (relevantSalaryData != null) { | |||
| return financialYear.copy( | |||
| @@ -2455,7 +2414,7 @@ open class ReportService( | |||
| val previousHourlyRate = findPreviousValue(salaryDataList, financialYear.start) { it.hourlyRate } | |||
| val previousSalaryPoint = findPreviousValue(salaryDataList, financialYear.start) { it.salaryPoint } | |||
| println("====================== staffInfo: $staffInfo ===============================") | |||
| // println("====================== staffInfo: $staffInfo ===============================") | |||
| return financialYear.copy( | |||
| hourlyRate = previousHourlyRate?.toDouble() ?: financialYear.hourlyRate, | |||
| salaryPoint = previousSalaryPoint ?: financialYear.salaryPoint | |||
| @@ -2497,6 +2456,7 @@ open class ReportService( | |||
| } | |||
| // 6 months as a period | |||
| // PeriodStartMonth can be changed depends on the client, now we will be using Jan as Start Month | |||
| fun getHalfYearFinancialPeriods( | |||
| startDate: YearMonth, | |||
| endDate: YearMonth, | |||
| @@ -2631,7 +2591,7 @@ open class ReportService( | |||
| val convertEndMonth = YearMonth.of(endDate.year, endDate.month) | |||
| val monthRange: MutableList<Map<String, Any>> = ArrayList() | |||
| val financialYears = getHalfYearFinancialPeriods(queryStartMonth, queryEndMonth, 10) | |||
| val financialYears = getHalfYearFinancialPeriods(queryStartMonth, queryEndMonth, 1) | |||
| var currentDate = startDate | |||
| if (currentDate == endDate) { | |||
| @@ -2929,8 +2889,8 @@ open class ReportService( | |||
| } | |||
| val totalManhourECell = totalManhourERow.getCell(1) ?: totalManhourERow.createCell(1) | |||
| totalManhourECell.apply { | |||
| // cellFormula = "SUM(${lastColumnIndex}${startRow}:${lastColumnIndex}${startRow + staffInfoList.size})" | |||
| setCellValue(info.getValue("manhourExpenditure") as Double) | |||
| cellFormula = "SUM(${lastColumnIndex}${startRow}:${lastColumnIndex}${startRow + staffInfoList.size})" | |||
| // setCellValue(info.getValue("manhourExpenditure") as Double) | |||
| cellStyle.dataFormat = accountingStyle | |||
| } | |||
| CellUtil.setCellStyleProperty(totalManhourETitleCell, "borderBottom", BorderStyle.THIN) | |||
| @@ -2985,31 +2945,19 @@ open class ReportService( | |||
| fun getCostAndExpense(clientId: Long?, teamId: Long?, type: String): List<Map<String, Any?>> { | |||
| 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" | |||
| + " )" | |||
| + " select p.code, p.description, c.name as client, IFNULL(s2.name, \'NA\') as subsidiary, concat(t.code, \' - \', t.name) as teamLead, p.expectedTotalFee, ifnull(p.subContractFee, 0) as subContractFee," | |||
| + " SUM(IFNULL(cte_ts.normalConsumed, 0)) as normalConsumed," | |||
| + " SUM(IFNULL(cte_ts.otConsumed, 0)) as otConsumed," | |||
| + " IFNULL(cte_ts.hourlyRate, 0) as hourlyRate" | |||
| + " 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 customer_subsidiary cs on cs.id = p.customerSubsidiaryId" | |||
| + " left join subsidiary s2 on s2.id = cs.subsidiaryId " | |||
| + " left join tsmsdb.team t on t.teamLead = p.teamLead" | |||
| + " 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 ISNULL(p.code) = False" | |||
| " select p.code, p.description, c.name as client, IFNULL(s2.name, \"N/A\") as subsidiary, " + | |||
| " concat(t.code, ' - ', t.name) as teamLead, ifnull(p.expectedTotalFee, 0) as expectedTotalFee, " + | |||
| " ifnull(p.subContractFee, 0) as subContractFee, " + | |||
| " 0 as totalCumulativeExpenditure " + | |||
| " from project p " + | |||
| " left join customer c on c.id = p.customerId " + | |||
| " left join customer_subsidiary cs on cs.id = p.customerSubsidiaryId " + | |||
| " left join subsidiary s2 on s2.id = cs.subsidiaryId " + | |||
| " left join tsmsdb.team t on t.teamLead = p.teamLead " + | |||
| " where ISNULL(p.code) = False " + | |||
| " order by p.code" | |||
| ) | |||
| if (clientId != null) { | |||
| if (type == "client") { | |||
| sql.append( | |||
| @@ -3025,57 +2973,39 @@ open class ReportService( | |||
| if (teamId != null) { | |||
| sql.append( | |||
| " and p.teamLead = :teamId " | |||
| " and p.teamLead = :teamLeadId " | |||
| ) | |||
| } | |||
| sql.append( | |||
| " group by p.code, p.description , c.name, teamLead, p.expectedTotalFee, p.subContractFee , hourlyRate, s2.name order by p.code" | |||
| ) | |||
| val args = mapOf( | |||
| "clientId" to clientId, | |||
| "teamId" to teamId | |||
| "teamLeadId" to teamId | |||
| ) | |||
| val manhoursSpent = getManHoursSpentByTeam(teamId) | |||
| val salaryEffectiveMap = getSalaryEffectiveByTeamLead(teamId) | |||
| val updatedTimesheetData = updateTimesheetDataWithEffectiveSalary(manhoursSpent, salaryEffectiveMap) | |||
| val projectsExpenditure = calculateProjectExpenditures(updatedTimesheetData) | |||
| val otFactor = BigDecimal(1).toDouble() | |||
| val queryList = jdbcDao.queryForList(sql.toString(), args) | |||
| val costAndExpenseList = mutableListOf<Map<String, Any?>>() | |||
| val costAndExpenseList = jdbcDao.queryForList(sql.toString(), args) | |||
| for (item in queryList) { | |||
| val hourlyRate = (item.getValue("hourlyRate") as BigDecimal).toDouble() | |||
| if (item["code"] !in costAndExpenseList.map { it["code"] }) { | |||
| costAndExpenseList.add( | |||
| mapOf( | |||
| "code" to item["code"], | |||
| "description" to item["description"], | |||
| "client" to item["client"], | |||
| "subsidiary" to item["subsidiary"], | |||
| "teamLead" to item["teamLead"], | |||
| "budget" to item["expectedTotalFee"] as Double - item["subContractFee"] as Double, | |||
| "totalManhours" to item["normalConsumed"] as Double + item["otConsumed"] as Double, | |||
| "manhourExpenditure" to (hourlyRate * item["normalConsumed"] as Double) | |||
| + (hourlyRate * item["otConsumed"] as Double * otFactor) | |||
| ) | |||
| ) | |||
| } else { | |||
| val existingMap = costAndExpenseList.find { it.containsValue(item["code"]) }!! | |||
| costAndExpenseList[costAndExpenseList.indexOf(existingMap)] = existingMap.toMutableMap().apply { | |||
| put( | |||
| "totalManhours", | |||
| get("totalManhours") as Double + (item["normalConsumed"] as Double + item["otConsumed"] as Double) | |||
| ) | |||
| put( | |||
| "manhourExpenditure", | |||
| get("manhourExpenditure") as Double + ((hourlyRate * item["normalConsumed"] as Double) | |||
| + (hourlyRate * item["otConsumed"] as Double * otFactor)) | |||
| ) | |||
| val updatedList = costAndExpenseList.map { item -> | |||
| val code = item["code"] as? String | |||
| val expenditure = projectsExpenditure[code] | |||
| item.toMutableMap().apply { | |||
| if (code != null && expenditure != null) { | |||
| this["totalCumulativeExpenditure"] = expenditure | |||
| } else { | |||
| this["totalCumulativeExpenditure"] = BigDecimal.ZERO | |||
| } | |||
| put("budget", item["expectedTotalFee"] as Double - item["subContractFee"] as Double) | |||
| } | |||
| } | |||
| val result = costAndExpenseList.map { item -> | |||
| val result = updatedList.map { item -> | |||
| val budget = (item["budget"] as? Double)?.times(0.8) ?: 0.0 | |||
| val budgetRemain = budget - (item["manhourExpenditure"] as? Double ?: 0.0) | |||
| val budgetRemain = budget - (item["totalCumulativeExpenditure"] as? Double ?: 0.0) | |||
| val remainingPercent = (budgetRemain / budget) | |||
| item.toMutableMap().apply { | |||
| put("budgetPercentage", remainingPercent) | |||
| @@ -3193,7 +3123,7 @@ open class ReportService( | |||
| CellUtil.setCellStyleProperty(cell6, "dataFormat", accountingStyle) | |||
| val cell7 = row.getCell(7) ?: row.createCell(7) | |||
| val manHoutsSpentCost = item["manhourExpenditure"] as Double | |||
| val manHoutsSpentCost = (item["totalCumulativeExpenditure"] as BigDecimal).toDouble() | |||
| cell7.apply { | |||
| setCellValue(manHoutsSpentCost) | |||
| } | |||
| @@ -3842,4 +3772,239 @@ open class ReportService( | |||
| return workbook | |||
| } | |||
| // Use to Calculate cummunlative expenditure | |||
| data class TimesheetData( | |||
| val normalConsumed: Double, | |||
| val otConsumed: Double, | |||
| val recordDate: LocalDate, | |||
| val staffId: Long, | |||
| val hourlyRate: BigDecimal, | |||
| val salaryPoint: Int, | |||
| val projectCode: String, | |||
| val planStart: LocalDate, | |||
| val planEnd: LocalDate | |||
| ) | |||
| data class SalaryEffectiveInfo( | |||
| val staffId: Long, | |||
| val effectiveDate: LocalDate, | |||
| val hourlyRate: BigDecimal, | |||
| val salaryPoint: Int | |||
| ) | |||
| data class ProjectSummary( | |||
| val staffData: Map<Long, StaffSummary>, | |||
| val projectCumulativeExpenditure: BigDecimal | |||
| ) | |||
| data class StaffSummary( | |||
| val monthlyData: Map<YearMonth, MonthSummary>, | |||
| val staffCumulativeExpenditure: BigDecimal | |||
| ) | |||
| data class MonthSummary( | |||
| val hourlyRate: BigDecimal, | |||
| val salaryPoint: Int, | |||
| val totalNormalHours: Double, | |||
| val totalOTHours: Double | |||
| ) { | |||
| val otFactor = BigDecimal(1.0) | |||
| val totalHours: Double = totalNormalHours + totalOTHours | |||
| val totalCost: BigDecimal = (hourlyRate * BigDecimal(totalNormalHours) + BigDecimal(totalOTHours) * otFactor * hourlyRate).setScale(2, RoundingMode.HALF_UP) | |||
| } | |||
| // Get all the timesheet data by Team Lead | |||
| fun getManHoursSpentByTeam(teamLeadId: Long?): List<TimesheetData>{ | |||
| val sql = StringBuilder( | |||
| "select coalesce(t.normalConsumed, 0) as normalConsumed, coalesce(t.otConsumed, 0) as otConsumed, t.recordDate, t.staffId, s2.hourlyRate, s2.salaryPoint, p.code, p.planStart, p.planEnd" | |||
| + " from timesheet t" | |||
| + " left join project p on t.projectId = p.id" | |||
| + " left join staff s on t.staffId = s.id" | |||
| + " left join salary s2 on s.salaryId = s2.salaryPoint" | |||
| + " where t.projectId in" | |||
| + " (" | |||
| + " select p.id from project p" | |||
| + " where p.status = 'On-going'" | |||
| ) | |||
| if (teamLeadId != null){ | |||
| sql.append( "and p.teamLead = :teamLeadId " ) | |||
| } | |||
| sql.append(" ) and t.recordDate >= p.actualStart ") | |||
| sql.append(" order by code, recordDate, staffId; ") | |||
| val results = jdbcDao.queryForList(sql.toString(), mapOf("teamLeadId" to teamLeadId)).map { | |||
| result -> | |||
| TimesheetData( | |||
| result["normalConsumed"] as Double, | |||
| result["otConsumed"] as Double, | |||
| (result["recordDate"] as java.sql.Date).toLocalDate(), | |||
| (result["staffId"] as Int).toLong(), | |||
| result["hourlyRate"] as BigDecimal, | |||
| result["salaryPoint"] as Int, | |||
| result["code"] as String, | |||
| (result["planStart"] as java.sql.Date).toLocalDate(), | |||
| (result["planEnd"] as java.sql.Date).toLocalDate() | |||
| ) | |||
| } | |||
| return results | |||
| } | |||
| // Get corresponding Salary Effective Data by Team Lead | |||
| fun getSalaryEffectiveByTeamLead(teamLeadId: Long?): Map<Long, List<SalaryEffectiveInfo>> { | |||
| val sql = StringBuilder( | |||
| " select se.*, s.hourlyRate, s.salaryPoint" | |||
| + " from salary_effective se" | |||
| + " left join salary s on s.salaryPoint = se.salaryId" | |||
| + " where se.staffId in" | |||
| + " (" | |||
| + " select distinct t.staffId" | |||
| + " from timesheet t" | |||
| + " left join project p on t.projectId = p.id" | |||
| + " where t.projectId in" | |||
| + " (" | |||
| + " select p.id from project p" | |||
| + " where p.status = 'On-going'" | |||
| ) | |||
| if(teamLeadId != null){ | |||
| sql.append(" and p.teamLead = :teamLeadId ") | |||
| } | |||
| sql.append(" )) order by staffId, salaryId, date ") | |||
| val results = jdbcDao.queryForList(sql.toString(), mapOf("teamLeadId" to teamLeadId)).map { | |||
| result -> | |||
| SalaryEffectiveInfo( | |||
| (result["staffId"] as Int).toLong(), | |||
| (result["date"] as java.sql.Date).toLocalDate(), | |||
| result["hourlyRate"] as BigDecimal, | |||
| result["salaryPoint"] as Int, | |||
| ) | |||
| }.groupBy { it.staffId } | |||
| return results | |||
| } | |||
| // Update corresponding hourly rate and salary point if there is salary modification during the project period | |||
| fun updateTimesheetDataWithEffectiveSalary( | |||
| timesheetDataList: List<TimesheetData>, | |||
| salaryEffectiveMap: Map<Long, List<SalaryEffectiveInfo>> | |||
| ): List<TimesheetData> { | |||
| return timesheetDataList.map { timesheetData -> | |||
| // Check if the staffId exists in the salaryEffectiveMap | |||
| if (salaryEffectiveMap.containsKey(timesheetData.staffId)) { | |||
| val effectiveSalaryList = salaryEffectiveMap[timesheetData.staffId]!! | |||
| // Find the nearest effective date that is less than or equal to the record date | |||
| val nearestEffectiveSalary = effectiveSalaryList | |||
| .filter { YearMonth.from(it.effectiveDate) <= YearMonth.from(timesheetData.recordDate) } | |||
| .maxByOrNull { it.effectiveDate } | |||
| if (nearestEffectiveSalary != null) { | |||
| timesheetData.copy( | |||
| hourlyRate = nearestEffectiveSalary.hourlyRate, | |||
| salaryPoint = nearestEffectiveSalary.salaryPoint | |||
| ) | |||
| } else { | |||
| timesheetData | |||
| } | |||
| } else { | |||
| // If the staffId is not in the salaryEffectiveMap, return the original timesheetData | |||
| timesheetData | |||
| } | |||
| } | |||
| } | |||
| // Calculate the project expenditure, and group by project Code | |||
| // { | |||
| // "M-0534": { | |||
| // "3": 294953.13, | |||
| // "184": 4564389.06, | |||
| // "47": 148769.82, | |||
| // "36": 592376.60, | |||
| // "179": 127758.41, | |||
| // "124": 4478.13, | |||
| // "123": 287.50, | |||
| // "116": 1575.00 | |||
| // }, | |||
| // "M-0553": { | |||
| // "184": 1932268.75, | |||
| // "46": 269365.11, | |||
| // "47": 717492.84, | |||
| // "18": 64716.51, | |||
| // "3": 2956.25, | |||
| // "124": 9084.38, | |||
| // "123": 59.38 | |||
| // }, | |||
| // ... | |||
| // } | |||
| fun calculateProjectExpenditures(timesheetDataList: List<TimesheetData>): Map<String, BigDecimal> { | |||
| val otFactor = BigDecimal(1.0) | |||
| return timesheetDataList | |||
| .groupBy { it.projectCode } | |||
| .mapValues { (_, projectTimesheets) -> | |||
| projectTimesheets.fold(BigDecimal.ZERO) { acc, timesheet -> | |||
| val normalExpenditure = timesheet.hourlyRate * BigDecimal(timesheet.normalConsumed) | |||
| val otExpenditure = timesheet.hourlyRate * BigDecimal(timesheet.otConsumed) * otFactor | |||
| acc + normalExpenditure + otExpenditure | |||
| }.setScale(2, RoundingMode.HALF_UP) | |||
| } | |||
| } | |||
| // Update timesheet data with salary effective data, then group by project code, group by staff Id and group by Year Month | |||
| // Data foramt: | |||
| // "M-0976": { | |||
| // "184": { | |||
| // "2021-02": { | |||
| // "hourlyRate": 256.25, | |||
| // "salaryPoint": 18, | |||
| // "totalNormalHours": 36.0, | |||
| // "totalOTHours": 0.0, | |||
| // "totalHours": 36.0, | |||
| // "totalCost": 9225.00 | |||
| // }, | |||
| // "2021-03": { | |||
| // "hourlyRate": 256.25, | |||
| // "salaryPoint": 18, | |||
| // "totalNormalHours": 33.0, | |||
| // "totalOTHours": 0.0, | |||
| // "totalHours": 33.0, | |||
| // "totalCost": 8456.25 | |||
| // }, | |||
| // ... | |||
| // } | |||
| fun sumTimesheetDataByMonth(timesheetDataList: List<TimesheetData>): Map<String, ProjectSummary> { | |||
| return timesheetDataList | |||
| .groupBy { it.projectCode } | |||
| .mapValues { (_, projectTimesheets) -> | |||
| val staffData = projectTimesheets.groupBy { it.staffId } | |||
| .mapValues { (_, staffTimesheets) -> | |||
| val monthlyData = staffTimesheets.groupBy { timesheet -> | |||
| YearMonth.from(timesheet.recordDate) | |||
| }.mapValues { (_, monthTimesheets) -> | |||
| MonthSummary( | |||
| hourlyRate = monthTimesheets.maxByOrNull { it.recordDate }?.hourlyRate ?: BigDecimal.ZERO, | |||
| salaryPoint = monthTimesheets.maxByOrNull { it.recordDate }?.salaryPoint ?: 0, | |||
| totalNormalHours = monthTimesheets.sumOf { it.normalConsumed }, | |||
| totalOTHours = monthTimesheets.sumOf { it.otConsumed } | |||
| ) | |||
| } | |||
| val staffCumulativeExpenditure = monthlyData.values | |||
| .sumOf { it.totalCost } | |||
| .setScale(2, RoundingMode.HALF_UP) | |||
| StaffSummary(monthlyData, staffCumulativeExpenditure) | |||
| } | |||
| val projectCumulativeExpenditure = staffData.values | |||
| .sumOf { it.staffCumulativeExpenditure } | |||
| .setScale(2, RoundingMode.HALF_UP) | |||
| ProjectSummary(staffData, projectCumulativeExpenditure) | |||
| } | |||
| } | |||
| } | |||