Bladeren bron

Merge branch 'master' of https://git.2fi-solutions.com/davidhui/TSMS-backend

# Conflicts:
#	src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt
tags/Baseline_30082024_BACKEND_UAT
MSI\2Fi 1 jaar geleden
bovenliggende
commit
a3dee0a3d2
9 gewijzigde bestanden met toevoegingen van 232 en 42 verwijderingen
  1. +10
    -8
      src/main/java/com/ffii/tsms/modules/data/service/SalaryEffectiveService.kt
  2. +2
    -2
      src/main/java/com/ffii/tsms/modules/data/service/StaffsService.kt
  3. +32
    -22
      src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt
  4. +5
    -0
      src/main/java/com/ffii/tsms/modules/timesheet/entity/Timesheet.kt
  5. +83
    -0
      src/main/java/com/ffii/tsms/modules/timesheet/service/LeaveService.kt
  6. +71
    -10
      src/main/java/com/ffii/tsms/modules/timesheet/service/TimesheetsService.kt
  7. +23
    -0
      src/main/java/com/ffii/tsms/modules/timesheet/web/TimesheetsController.kt
  8. +6
    -0
      src/main/resources/db/changelog/changes/20240725_01_wayne/01_update_timesheet_non_billable_task.sql
  9. BIN
      src/main/resources/templates/report/Cross Team Charge Report.xlsx

+ 10
- 8
src/main/java/com/ffii/tsms/modules/data/service/SalaryEffectiveService.kt Bestand weergeven

@@ -41,20 +41,22 @@ open class SalaryEffectiveService(
return if(result > 0) result.toLong() else -1
}

open fun saveSalaryEffective (staffId: Long, salaryId: Long): SalaryEffective {
val existSalaryEffective = findByStaffIdAndSalaryId(staffId, salaryId)

if (existSalaryEffective != null) {
open fun saveSalaryEffective (staffId: Long, salaryId: Long): SalaryEffective? {
// val existSalaryEffective = findByStaffIdAndSalaryId(staffId, salaryId)
//
// logger.info(existSalaryEffective)
// if (existSalaryEffective != null) {
val latestSalaryId = findLatestSalaryIdByStaffId(staffId)

// If latest salary id is same as current salary id, then skip
if (latestSalaryId == existSalaryEffective.salary.id) {
return existSalaryEffective
// if (latestSalaryId == existSalaryEffective.salary.salaryPoint.toLong()) {
if (latestSalaryId == salaryId) {
return null
}
}
// }

val staff = staffRepository.findById(staffId).orElseThrow()
val salary = salaryRepository.findById(salaryId).orElseThrow()
val salary = salaryRepository.findBySalaryPoint(salaryId).orElseThrow()
val salaryEffective = SalaryEffective().apply {
date = LocalDate.now()
this.staff = staff


+ 2
- 2
src/main/java/com/ffii/tsms/modules/data/service/StaffsService.kt Bestand weergeven

@@ -175,7 +175,7 @@ open class StaffsService(
staffSkillsetRepository.save(ss)
}
}
salaryEffectiveService.saveSalaryEffective(staff.id!!, salary.id!!)
salaryEffectiveService.saveSalaryEffective(staff.id!!, salary.salaryPoint.toLong())
return staff
}
@Transactional(rollbackFor = [Exception::class])
@@ -224,7 +224,7 @@ open class StaffsService(
this.department = department
}

salaryEffectiveService.saveSalaryEffective(staff.id!!, salary.id!!)
salaryEffectiveService.saveSalaryEffective(staff.id!!, salary.salaryPoint.toLong())

return staffRepository.save(staff)
}


+ 32
- 22
src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt Bestand weergeven

@@ -40,6 +40,7 @@ import java.time.ZoneId
import java.time.format.DateTimeParseException
import java.time.temporal.ChronoUnit
import java.util.*
import java.awt.Color
import kotlin.collections.ArrayList


@@ -84,6 +85,23 @@ open class ReportService(
sheetCF.addConditionalFormatting(regions, ruleNegative)
}

private fun conditionalFormattingPositive(sheet: Sheet) {
// Create a conditional formatting rule
val sheetCF = sheet.sheetConditionalFormatting

val color = XSSFColor(Color(255, 242, 204), null)
val formula = "AND(ISNUMBER(A1), A1>0)"
val rulePositive = sheetCF.createConditionalFormattingRule(formula).apply {
createPatternFormatting().apply {
setFillBackgroundColor(color)
}
}

val lastCell = sheet.maxOf { it.lastCellNum - 1 }
val regions = arrayOf(CellRangeAddress(0, sheet.lastRowNum, 0, lastCell))
sheetCF.addConditionalFormatting(regions, rulePositive)
}

// ==============================|| GENERATE REPORT ||============================== //
fun generalCreateReportIndexed( // just loop through query records one by one, return rowIndex
sheet: Sheet,
@@ -2121,7 +2139,7 @@ open class ReportService(
else -> ""
}
}
sql.append(" group by p.code, p.name, tm.code, c.code, c.name, ss.code, ss.name, p.expectedTotalFee, p.subContractFee, p.totalManhour, p.expectedTotalFee ")
sql.append(" group by p.code, p.name, tm.code, c.code, c.name, ss.code, ss.name, p.expectedTotalFee, p.subContractFee, p.totalManhour ")
sql.append(statusFilter)
return jdbcDao.queryForList(sql.toString(), args)
}
@@ -2228,7 +2246,6 @@ open class ReportService(
// All recorded man hours of the project
val manHoursSpent = jdbcDao.queryForList(sql.toString(), args)


val projectCodeSql = StringBuilder(
"select p.code, p.description from project p where p.deleted = false and p.id = :projectId"
)
@@ -2278,16 +2295,6 @@ open class ReportService(
info["description"] = item.getValue("description")
}

// val stadd = item.getValue("staffId") as String
// println("StaffId: ${stadd}")
// println(
// "HourlyRate: " +
// "${
// getSalaryForMonth(
// item.getValue("recordDate") as String,
// item.getValue("staffId") as String,
// staffSalaryLists
// )}")
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"] }) {
@@ -2470,7 +2477,6 @@ open class ReportService(
return financialYearDates
}


fun createPandLReportWorkbook(
templatePath: String,
manhoursSpent: List<Map<String, Any>>,
@@ -2880,7 +2886,7 @@ open class ReportService(
+ " 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"
+ " 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"
@@ -3318,17 +3324,18 @@ open class ReportService(

sortedGrades.forEach { grade: Grade ->
createCell(columnIndex++).apply {
setCellValue(grade.name)
setCellValue("${grade.name} - Hours")
val cloneStyle = workbook.createCellStyle()
cloneStyle.cloneStyleFrom(boldFontWithBorderStyle)
cellStyle = cloneStyle.apply {
alignment = HorizontalAlignment.CENTER
wrapText = true
}
}

createCell(columnIndex++).apply {
val cellValue = grade.name.trim().substring(0, 1) + grade.name.trim()
.substring(grade.name.length - 1) + " - Cost Adjusted by Salary Point"
.substring(grade.name.length - 1) + " - Cost (HKD)"
setCellValue(cellValue)
val cloneStyle = workbook.createCellStyle()
cloneStyle.cloneStyleFrom(boldFontWithBorderStyle)
@@ -3348,7 +3355,7 @@ open class ReportService(
}

createCell(columnIndex).apply {
setCellValue("Total Cost Adjusted by Salary Point by Team")
setCellValue("Total Cost (HKD) by Team")
val cloneStyle = workbook.createCellStyle()
cloneStyle.cloneStyleFrom(boldFontWithBorderStyle)
cellStyle = cloneStyle.apply {
@@ -3485,6 +3492,8 @@ open class ReportService(
// }

conditionalFormattingNegative(sheet)
conditionalFormattingPositive(sheet)

// -------------------------- sheet 1 (Individual) -------------------------- //
sheet = workbook.getSheetAt(1)

@@ -3530,20 +3539,20 @@ open class ReportService(
sortedTeams.forEach { team: Team ->
// not his/her team staffs
val staffs = timesheets
.filter { it.project?.teamLead?.team?.id != it.staff?.team?.id && (it.project?.teamLead?.id != team.staff.id || it.staff?.team?.id != team.id) }
.filter { it.project?.teamLead?.team?.id == team.id && it.staff?.team?.id != team.id }
.mapNotNull { it.staff }
.sortedBy { it.staffId }
.distinct()

// his/her team projects
val projects = timesheets
.filter { it.project?.teamLead?.team?.id != it.staff?.team?.id && it.project?.teamLead?.id == team.staff.id }
.filter { it.project?.teamLead?.team?.id == team.id && it.project?.teamLead?.team?.id != it.staff?.team?.id }
.mapNotNull { it.project }
.sortedByDescending { it.code }
.distinct()

// Team
if (!projects.isNullOrEmpty()) {
if (projects.isNotEmpty()) {
sheet.createRow(rowIndex++).apply {
createCell(0).apply {
setCellValue("Team to be charged:")
@@ -3618,7 +3627,7 @@ open class ReportService(
var endRow = rowIndex
projects.forEach { project: Project ->
if (teamId.lowercase() == "all" || teamId.toLong() == project.teamLead?.team?.id || teamId.toLong() == team.id) {
if (team.id == project.teamLead?.team?.id) {
// if (team.id == project.teamLead?.team?.id) {
endRow++
sheet.createRow(rowIndex++).apply {
columnIndex = 0
@@ -3674,7 +3683,7 @@ open class ReportService(
}
}
}
}
// }
}
}

@@ -3720,6 +3729,7 @@ open class ReportService(
}

conditionalFormattingNegative(sheet)
conditionalFormattingPositive(sheet)

return workbook
}

+ 5
- 0
src/main/java/com/ffii/tsms/modules/timesheet/entity/Timesheet.kt Bestand weergeven

@@ -4,6 +4,7 @@ import com.ffii.core.entity.BaseEntity
import com.ffii.tsms.modules.data.entity.Staff
import com.ffii.tsms.modules.project.entity.Project
import com.ffii.tsms.modules.project.entity.ProjectTask
import com.ffii.tsms.modules.project.entity.Task
import jakarta.persistence.*
import jakarta.validation.constraints.NotNull
import java.time.LocalDate
@@ -30,6 +31,10 @@ open class Timesheet : BaseEntity<Long>() {
@JoinColumn(name = "projectTaskId")
open var projectTask: ProjectTask? = null

@ManyToOne
@JoinColumn(name = "nonBillableTaskId")
open var nonBillableTask: Task? = null

@ManyToOne
@JoinColumn(name = "projectId")
open var project: Project? = null


+ 83
- 0
src/main/java/com/ffii/tsms/modules/timesheet/service/LeaveService.kt Bestand weergeven

@@ -1,22 +1,29 @@
package com.ffii.tsms.modules.timesheet.service

import com.ffii.core.exception.BadRequestException
import com.ffii.tsms.modules.data.entity.StaffRepository
import com.ffii.tsms.modules.data.service.StaffsService
import com.ffii.tsms.modules.data.service.TeamService
import com.ffii.tsms.modules.timesheet.entity.*
import com.ffii.tsms.modules.timesheet.web.models.LeaveEntry
import com.ffii.tsms.modules.timesheet.web.models.TeamMemberLeaveEntries
import org.apache.commons.logging.LogFactory
import org.apache.poi.ss.usermodel.Sheet
import org.apache.poi.ss.usermodel.Workbook
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.LocalDate
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import kotlin.jvm.optionals.getOrDefault
import kotlin.jvm.optionals.getOrNull

@Service
open class LeaveService(
private val leaveRepository: LeaveRepository,
private val leaveTypeRepository: LeaveTypeRepository,
private val staffsService: StaffsService,
private val staffRepository: StaffRepository,
private val teamService: TeamService
) {
open fun getLeaveTypes(): List<LeaveType> {
@@ -130,4 +137,80 @@ open class LeaveService(
)
} }
}

@Transactional(rollbackFor = [Exception::class])
open fun importFile(workbook: Workbook?): String {
val logger = LogFactory.getLog(javaClass)

if (workbook == null) {
return "No Excel import" // if workbook is null
}

val notExistStaffList = mutableListOf<String>()
val sheet: Sheet = workbook.getSheetAt(0)

logger.info("---------Start Import Leaves-------")
val leaveList = mutableListOf<Leave>().toMutableList();
for (i in 1..<sheet.lastRowNum) {
val row = sheet.getRow(i)

if (row.getCell(1) != null && !row.getCell(1).stringCellValue.isNullOrBlank()) {
logger.info("row :$i | lastCellNum" + row.lastCellNum)

// process staff
logger.info("---------staff-------")
val staff = staffRepository.findByStaffId(row.getCell(1).stringCellValue).getOrNull()
if (staff == null) {
notExistStaffList += row.getCell(1).stringCellValue
// staff = staffRepository.findByStaffId("B000").orElseThrow()
}

if (staff != null) {
// process leave type
logger.info("---------leave type-------")
val leaveTypeCellValue = row.getCell(4).stringCellValue
val leaveType = when (leaveTypeCellValue) {
"LA" -> {
leaveTypeRepository.findById(1).getOrNull()
}
"LS" -> {
leaveTypeRepository.findById(2).getOrNull()
}
else -> {
leaveTypeRepository.findById(3).getOrNull()
}
}
val remark = if (leaveTypeCellValue == "LA" || leaveTypeCellValue == "LS") {
null
} else {
leaveTypeCellValue
}

// process record date
logger.info("---------record date-------")
val formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy")
logger.info("Date: ${row.getCell(7).dateCellValue.toInstant().atZone(ZoneId.systemDefault()).toLocalDate()}")
val recordDate = row.getCell(7).dateCellValue.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();

// hour
logger.info("---------hour-------")
val leaveHours: Double = row.getCell(8).numericCellValue

leaveList += Leave().apply {
this.staff = staff
this.recordDate = recordDate
this.leaveHours = leaveHours
this.leaveType = leaveType
this.remark = remark
}
}
}
}

leaveRepository.saveAll(leaveList)
logger.info("---------end-------")
logger.info("Not Exist Staff List: "+ notExistStaffList.distinct().joinToString(", "))

return if (sheet.lastRowNum > 0) "Import Excel success btw not Exist: " + notExistStaffList.distinct().joinToString(", ") else "Import Excel failure"
}
}

+ 71
- 10
src/main/java/com/ffii/tsms/modules/timesheet/service/TimesheetsService.kt Bestand weergeven

@@ -1,15 +1,12 @@
package com.ffii.tsms.modules.timesheet.service

import com.ffii.core.exception.BadRequestException
import com.ffii.core.utils.ExcelUtils
import com.ffii.tsms.modules.data.entity.BuildingType
import com.ffii.tsms.modules.data.entity.Staff
import com.ffii.tsms.modules.data.entity.StaffRepository
import com.ffii.tsms.modules.data.entity.WorkNature
import com.ffii.tsms.modules.data.service.StaffsService
import com.ffii.tsms.modules.data.service.TeamService
import com.ffii.tsms.modules.project.entity.*
import com.ffii.tsms.modules.project.web.models.*
import com.ffii.tsms.modules.timesheet.entity.Leave
import com.ffii.tsms.modules.timesheet.entity.LeaveRepository
import com.ffii.tsms.modules.timesheet.entity.Timesheet
import com.ffii.tsms.modules.timesheet.entity.TimesheetRepository
import com.ffii.tsms.modules.timesheet.web.models.TeamMemberTimeEntries
@@ -31,7 +28,7 @@ open class TimesheetsService(
private val projectRepository: ProjectRepository,
private val taskRepository: TaskRepository,
private val staffsService: StaffsService,
private val teamService: TeamService, private val staffRepository: StaffRepository
private val teamService: TeamService, private val staffRepository: StaffRepository, private val leaveRepository: LeaveRepository
) {
@Transactional
open fun saveTimesheet(recordTimeEntry: Map<LocalDate, List<TimeEntry>>): Map<String, List<TimeEntry>> {
@@ -56,6 +53,7 @@ open class TimesheetsService(
this.projectTask = projectTask
this.project = project
this.remark = timeEntry.remark
this.nonBillableTask = if (project == null) task else null
}
}
}
@@ -94,6 +92,7 @@ open class TimesheetsService(
this.remark = entry.remark
this.recordDate = this.recordDate ?: recordDate
this.staff = this.staff ?: memberStaff
this.nonBillableTask = if (project == null) task else null
}

timesheetRepository.save(timesheet)
@@ -149,11 +148,12 @@ open class TimesheetsService(
.groupBy { timesheet -> timesheet.recordDate!!.format(DateTimeFormatter.ISO_LOCAL_DATE) }
.mapValues { (_, timesheets) ->
timesheets.map { timesheet ->
val projectTask = timesheet.projectTask
TimeEntry(
id = timesheet.id!!,
projectId = timesheet.projectTask?.project?.id ?: timesheet.project?.id,
taskId = timesheet.projectTask?.task?.id,
taskGroupId = timesheet.projectTask?.task?.taskGroup?.id,
projectId = projectTask?.project?.id ?: timesheet.project?.id,
taskId = (projectTask?.task ?: timesheet.nonBillableTask)?.id,
taskGroupId = (projectTask?.task ?: timesheet.nonBillableTask)?.taskGroup?.id,
inputHours = timesheet.normalConsumed ?: 0.0,
otHours = timesheet.otConsumed ?: 0.0,
remark = timesheet.remark
@@ -246,6 +246,67 @@ open class TimesheetsService(
logger.info("---------end-------")
logger.info("Not Exist Project List: "+ notExistProjectList.distinct().joinToString(", "))

return if (sheet.lastRowNum > 0) "Import Excel success btw " + notExistProjectList.joinToString(", ") else "Import Excel failure"
return if (sheet.lastRowNum > 0) "Import Excel success btw " + notExistProjectList.distinct().joinToString(", ") else "Import Excel failure"
}

@Transactional(rollbackFor = [Exception::class])
open fun rearrangeTimesheets(): String {
val logger = LogFactory.getLog(javaClass)

val timesheets = timesheetRepository.findAll()
.filter { it.deleted == false }
.sortedBy { it.id }

var newTimesheetsList = mutableListOf<Timesheet>()

val leaves = leaveRepository.findAll()
.filter { it.deleted == false }
.groupBy { leave: Leave -> Pair(leave.staff?.id, leave.recordDate) }
.mapValues { (_, leave) ->
leave.sumOf { it.leaveHours ?: 0.0 }
}

timesheets.forEach { timesheet: Timesheet ->
if (timesheet.staff?.staffId != "B000") {
val leaveHours = leaves[Pair(timesheet.staff?.id, timesheet.recordDate)] ?: 0.0

val timesheetHours = newTimesheetsList
.filter { it.recordDate?.equals(timesheet.recordDate) == true && it.staff?.id == timesheet.staff?.id}
.sumOf { (it.normalConsumed ?: 0.0) + (it.otConsumed ?: 0.0) }

val previousHours = leaveHours + timesheetHours
val currentHours = (timesheet.normalConsumed ?: 0.0) + (timesheet.otConsumed ?: 0.0)
val totalHours = previousHours + currentHours

val newNormalConsumed = when {
totalHours <= 8.0 -> currentHours
else -> maxOf(8.0 - previousHours, 0.0)
}

val newOtConsumed = when {
totalHours <= 8.0 -> 0.0
else -> maxOf(currentHours - newNormalConsumed, 0.0)
}

logger.info("-----------------------------------------")
logger.info("ID: ${timesheet.id}")
logger.info("Staff: ${timesheet.staff?.staffId} | Record Date: ${timesheet.recordDate}")
logger.info("totalHours: ${totalHours} | " +
"Current Normal Consumed: ${(timesheet.normalConsumed ?: 0.0)} | " +
"Current OT Consumed: ${(timesheet.otConsumed ?: 0.0)} | " +
"Leave Hours: ${leaveHours} | " +
"New Normal Consumed: ${newNormalConsumed} | " +
"New OT Consumed: ${newOtConsumed}")

newTimesheetsList += timesheet.apply {
normalConsumed = newNormalConsumed
otConsumed = newOtConsumed
}
}
}
logger.info("END")
timesheetRepository.saveAll(newTimesheetsList)

return "Rearrange success"
}
}

+ 23
- 0
src/main/java/com/ffii/tsms/modules/timesheet/web/TimesheetsController.kt Bestand weergeven

@@ -136,4 +136,27 @@ class TimesheetsController(private val timesheetsService: TimesheetsService, pri

return ResponseEntity.ok(timesheetsService.importFile(workbook))
}

@PostMapping("/import-leave")
@Throws(ServletRequestBindingException::class)
fun importLeaveFile(request: HttpServletRequest): ResponseEntity<*> {
var workbook: Workbook? = null

try {
val multipartFile = (request as MultipartHttpServletRequest).getFile("multipartFileList")
workbook = XSSFWorkbook(multipartFile?.inputStream)
} catch (e: Exception) {
println("Excel Wrong")
println(e)
}

return ResponseEntity.ok(leaveService.importFile(workbook))
}

@PostMapping("/rearrange")
@Throws(ServletRequestBindingException::class)
fun rearrangeTimesheets(request: HttpServletRequest): ResponseEntity<*> {

return ResponseEntity.ok(timesheetsService.rearrangeTimesheets())
}
}

+ 6
- 0
src/main/resources/db/changelog/changes/20240725_01_wayne/01_update_timesheet_non_billable_task.sql Bestand weergeven

@@ -0,0 +1,6 @@
-- liquibase formatted sql
-- changeset wayne:update_timesheet_non_billable_task

ALTER TABLE timesheet ADD nonBillableTaskId INT NULL;

ALTER TABLE timesheet ADD CONSTRAINT FK_TIMESHEET_ON_NONBILLABLETASKID FOREIGN KEY (nonBillableTaskId) REFERENCES task (id);

BIN
src/main/resources/templates/report/Cross Team Charge Report.xlsx Bestand weergeven


Laden…
Annuleren
Opslaan