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