소스 검색

update salary effective, staff, report, leave, timesheet

tags/Baseline_30082024_BACKEND_UAT
cyril.tsui 1 년 전
부모
커밋
af3b965005
7개의 변경된 파일215개의 추가작업 그리고 23개의 파일을 삭제
  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
    -11
      src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt
  4. +83
    -0
      src/main/java/com/ffii/tsms/modules/timesheet/service/LeaveService.kt
  5. +65
    -2
      src/main/java/com/ffii/tsms/modules/timesheet/service/TimesheetsService.kt
  6. +23
    -0
      src/main/java/com/ffii/tsms/modules/timesheet/web/TimesheetsController.kt
  7. BIN
      src/main/resources/templates/report/Cross Team Charge Report.xlsx

+ 10
- 8
src/main/java/com/ffii/tsms/modules/data/service/SalaryEffectiveService.kt 파일 보기

@@ -39,20 +39,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 파일 보기

@@ -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
- 11
src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt 파일 보기

@@ -37,7 +37,7 @@ import java.time.ZoneId
import java.time.format.DateTimeParseException
import java.time.temporal.ChronoUnit
import java.util.*
import java.awt.Color

data class DayInfo(val date: String?, val weekday: String?)

@@ -79,6 +79,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,
@@ -2116,7 +2133,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)
}
@@ -2755,7 +2772,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"
@@ -3193,17 +3210,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)
@@ -3223,7 +3241,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 {
@@ -3360,6 +3378,8 @@ open class ReportService(
// }

conditionalFormattingNegative(sheet)
conditionalFormattingPositive(sheet)

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

@@ -3405,20 +3425,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:")
@@ -3493,7 +3513,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
@@ -3549,7 +3569,7 @@ open class ReportService(
}
}
}
}
// }
}
}

@@ -3595,6 +3615,7 @@ open class ReportService(
}

conditionalFormattingNegative(sheet)
conditionalFormattingPositive(sheet)

return workbook
}

+ 83
- 0
src/main/java/com/ffii/tsms/modules/timesheet/service/LeaveService.kt 파일 보기

@@ -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"
}
}

+ 65
- 2
src/main/java/com/ffii/tsms/modules/timesheet/service/TimesheetsService.kt 파일 보기

@@ -10,6 +10,8 @@ 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 +33,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>> {
@@ -246,6 +248,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 파일 보기

@@ -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())
}
}

BIN
src/main/resources/templates/report/Cross Team Charge Report.xlsx 파일 보기


불러오는 중...
취소
저장