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 4ab6e32..efda40a 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 @@ -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 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 9010c7b..b492d98 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 @@ -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>, @@ -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 } diff --git a/src/main/java/com/ffii/tsms/modules/timesheet/entity/Timesheet.kt b/src/main/java/com/ffii/tsms/modules/timesheet/entity/Timesheet.kt index 1566126..e83e980 100644 --- a/src/main/java/com/ffii/tsms/modules/timesheet/entity/Timesheet.kt +++ b/src/main/java/com/ffii/tsms/modules/timesheet/entity/Timesheet.kt @@ -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() { @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 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..96f7320 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 @@ -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>): Map> { @@ -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() + + 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/db/changelog/changes/20240725_01_wayne/01_update_timesheet_non_billable_task.sql b/src/main/resources/db/changelog/changes/20240725_01_wayne/01_update_timesheet_non_billable_task.sql new file mode 100644 index 0000000..c1d3081 --- /dev/null +++ b/src/main/resources/db/changelog/changes/20240725_01_wayne/01_update_timesheet_non_billable_task.sql @@ -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); 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 54ec3cf..3beb992 100644 Binary files a/src/main/resources/templates/report/Cross Team Charge Report.xlsx and b/src/main/resources/templates/report/Cross Team Charge Report.xlsx differ