From af3b965005637031a3a1a1454f76e3aff23935d4 Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Wed, 24 Jul 2024 16:37:18 +0800 Subject: [PATCH] update salary effective, staff, report, leave, timesheet --- .../data/service/SalaryEffectiveService.kt | 18 ++-- .../modules/data/service/StaffsService.kt | 4 +- .../modules/report/service/ReportService.kt | 43 ++++++--- .../modules/timesheet/service/LeaveService.kt | 83 ++++++++++++++++++ .../timesheet/service/TimesheetsService.kt | 67 +++++++++++++- .../timesheet/web/TimesheetsController.kt | 23 +++++ .../report/Cross Team Charge Report.xlsx | Bin 16594 -> 16657 bytes 7 files changed, 215 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/ffii/tsms/modules/data/service/SalaryEffectiveService.kt b/src/main/java/com/ffii/tsms/modules/data/service/SalaryEffectiveService.kt index f78fd42..52aaf45 100644 --- a/src/main/java/com/ffii/tsms/modules/data/service/SalaryEffectiveService.kt +++ b/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 diff --git a/src/main/java/com/ffii/tsms/modules/data/service/StaffsService.kt b/src/main/java/com/ffii/tsms/modules/data/service/StaffsService.kt index d575011..22cae68 100644 --- a/src/main/java/com/ffii/tsms/modules/data/service/StaffsService.kt +++ b/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) } 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 356575c..f9a8b54 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 @@ -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 } diff --git a/src/main/java/com/ffii/tsms/modules/timesheet/service/LeaveService.kt b/src/main/java/com/ffii/tsms/modules/timesheet/service/LeaveService.kt index bdcb0ed..7b01cf7 100644 --- a/src/main/java/com/ffii/tsms/modules/timesheet/service/LeaveService.kt +++ b/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 { @@ -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() + val sheet: Sheet = workbook.getSheetAt(0) + + logger.info("---------Start Import Leaves-------") + val leaveList = mutableListOf().toMutableList(); + for (i in 1.. { + 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" + } } diff --git a/src/main/java/com/ffii/tsms/modules/timesheet/service/TimesheetsService.kt b/src/main/java/com/ffii/tsms/modules/timesheet/service/TimesheetsService.kt index 32bdf90..0445689 100644 --- a/src/main/java/com/ffii/tsms/modules/timesheet/service/TimesheetsService.kt +++ b/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>): Map> { @@ -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() + + 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" } } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/timesheet/web/TimesheetsController.kt b/src/main/java/com/ffii/tsms/modules/timesheet/web/TimesheetsController.kt index 68ccecf..4aa049e 100644 --- a/src/main/java/com/ffii/tsms/modules/timesheet/web/TimesheetsController.kt +++ b/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()) + } } \ No newline at end of file diff --git a/src/main/resources/templates/report/Cross Team Charge Report.xlsx b/src/main/resources/templates/report/Cross Team Charge Report.xlsx index 54ec3cf9b517bd86b16415630a4a2a5e3284e9de..3beb9924fa78bb8e713cdfbbdb6dd3aef4f5eabd 100644 GIT binary patch delta 3564 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 E0Sex)?EnA( delta 3528 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