From b2c47f46bae6ac0456a5aa253035b4bf35cba9d7 Mon Sep 17 00:00:00 2001 From: "MSI\\2Fi" Date: Fri, 9 Aug 2024 17:29:32 +0800 Subject: [PATCH] no message --- .../modules/report/service/ReportService.kt | 485 ++++++++++++------ 1 file changed, 325 insertions(+), 160 deletions(-) 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 44f7352..e041c1e 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 @@ -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> = getFinancialStatus(teamLeadId) - - val otFactor = BigDecimal(1) - - val tempList = mutableListOf>() - - 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> { 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>() @@ -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, start: YearMonth, end: YearMonth): Double? { + fun getSalaryForMonth(recordDate: String, staffId: String, staffSalaryLists: List): 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, 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, 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?, staffInfo: Map): 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> = 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> { 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>() + 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, + val projectCumulativeExpenditure: BigDecimal + ) + + data class StaffSummary( + val monthlyData: Map, + 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{ + 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> { + 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, + salaryEffectiveMap: Map> + ): List { + 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): Map { + 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): Map { + + 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) + } + } } \ No newline at end of file