@@ -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(cum Expenditure.toDouble())
setCellValue(totalCumulative Expenditure.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 = :teamLead Id "
)
}
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["manhour Expenditure"] 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)
}
}
}