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 2c2ec8f..c962145 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 @@ -4,16 +4,21 @@ import com.ffii.core.exception.UnprocessableEntityException import com.ffii.core.support.AbstractBaseEntityService import com.ffii.core.support.JdbcDao import com.ffii.tsms.modules.common.SecurityUtils +import com.ffii.tsms.modules.common.SettingNames +import com.ffii.tsms.modules.common.mail.pojo.MailRequest import com.ffii.tsms.modules.data.entity.* import com.ffii.tsms.modules.data.entity.projections.StaffSearchInfo import com.ffii.tsms.modules.data.web.models.NewStaffRequest import com.ffii.tsms.modules.data.web.models.SalaryEffectiveInfo import com.ffii.tsms.modules.user.entity.User import com.ffii.tsms.modules.user.entity.UserRepository +import jakarta.mail.internet.InternetAddress +import org.springframework.scheduling.annotation.Scheduled import org.springframework.security.core.GrantedAuthority import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional +import java.time.LocalDate import java.util.* import kotlin.jvm.optionals.getOrNull @@ -138,6 +143,11 @@ open class StaffsService( name = req.name phone1 = req.phone1 email = req.email ?: null + locked = if (req.departDate != null) { + LocalDate.now().isAfter(req.departDate) + } else { + false + } } ) @@ -226,6 +236,14 @@ open class StaffsService( this.department = department } + if (req.departDate != null) { + val user = userRepository.findByUsernameAndDeletedFalse(req.staffId).orElseThrow() + user.apply { + locked = LocalDate.now().isAfter(req.departDate) + } + + userRepository.save(user) + } // salaryEffectiveService.saveSalaryEffective(staff.id!!, salary.salaryPoint.toLong()) salaryEffectiveService.updateSalaryEffective(staff.id!!, req.salaryEffectiveInfo?.sortedBy { it.date }, req.delSalaryEffectiveInfo) @@ -267,4 +285,23 @@ open class StaffsService( val latestSalaryPoint = salaryEffectInfo?.maxByOrNull { it.date }?.salaryPoint return latestSalaryPoint } + + @Scheduled(cron = "0 0 0 * * ?") // Runs at 00:00 AM every day + open fun checkDepartStaffs() { + val staffs = staffRepository.findAll() + + val users = mutableListOf() + + staffs.forEach { staff -> + val user = userRepository.findByUsernameAndDeletedFalse(staff.staffId).orElseThrow() + + user.apply { + locked = LocalDate.now().isAfter(staff.departDate) + } + + users += user + } + + userRepository.saveAll(users) + } } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/project/entity/Project.kt b/src/main/java/com/ffii/tsms/modules/project/entity/Project.kt index 77a3e36..54267e8 100644 --- a/src/main/java/com/ffii/tsms/modules/project/entity/Project.kt +++ b/src/main/java/com/ffii/tsms/modules/project/entity/Project.kt @@ -69,6 +69,9 @@ open class Project : BaseEntity() { @Column(name = "totalManhour") open var totalManhour: Double? = null + @Column(name = "ratePerManhour") + open var ratePerManhour: Double? = null + @OneToMany(mappedBy = "project", cascade = [CascadeType.ALL], orphanRemoval = true) open var gradeAllocations: MutableSet = mutableSetOf() diff --git a/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt b/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt index 0892abd..01521fe 100644 --- a/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt +++ b/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt @@ -227,6 +227,7 @@ open class ProjectsService( expectedTotalFee = request.expectedProjectFee subContractFee = request.subContractFee totalManhour = request.totalManhour + ratePerManhour = request.ratePerManhour actualStart = request.projectActualStart actualEnd = request.projectActualEnd status = if (this.status == "Deleted" || this.deleted == true) "Deleted" @@ -729,6 +730,7 @@ open class ProjectsService( expectedProjectFee = row.getCell(9).numericCellValue, subContractFee = null, totalManhour = row.getCell(11).numericCellValue, + ratePerManhour = 250.0, locationId = 1, // HK buildingTypeIds = mutableListOf(buildingType!!.id!!), workNatureIds = mutableListOf(workNature!!.id!!), diff --git a/src/main/java/com/ffii/tsms/modules/project/web/models/NewProjectRequest.kt b/src/main/java/com/ffii/tsms/modules/project/web/models/NewProjectRequest.kt index b3214e6..413b0b2 100644 --- a/src/main/java/com/ffii/tsms/modules/project/web/models/NewProjectRequest.kt +++ b/src/main/java/com/ffii/tsms/modules/project/web/models/NewProjectRequest.kt @@ -41,6 +41,7 @@ data class NewProjectRequest( val manhourPercentageByGrade: Map, val taskGroups: Map, val allocatedStaffIds: List, + val ratePerManhour: Double, // Milestones val milestones: Map, 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 209a1d4..3cd765d 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 @@ -66,6 +66,7 @@ open class ReportService( private val COMPLETION_PROJECT = "templates/report/AR05_Project Completion Report.xlsx" private val CROSS_TEAM_CHARGE_REPORT = "templates/report/Cross Team Charge Report.xlsx" + private val chargeFee = 1.15 private fun conditionalFormattingNegative(sheet: Sheet) { // Create a conditional formatting rule val sheetCF = sheet.sheetConditionalFormatting @@ -778,7 +779,7 @@ open class ReportService( } createCell(2).apply { - setCellValue((project.expectedTotalFee ?: 0.0) - (project.subContractFee ?: 0.0)) + setCellValue(project.expectedTotalFee ?: 0.0) cellStyle.dataFormat = accountingStyle } } @@ -2117,46 +2118,31 @@ open class ReportService( open fun getProjectResourceOverconsumptionReport(args: Map): List> { val sql = StringBuilder( - "SELECT" - + " p.code, " - + " p.name, " - + " tm.code as team, " - + " concat(c.code, ' - ',c.name) as client, " - + " COALESCE(concat(ss.code, ' - ', ss.name), 'N/A') as subsidiary, " - + " (p.expectedTotalFee - ifnull(p.subContractFee, 0)) * 0.8 as plannedBudget, " - + " sum(t.consumedBudget) as actualConsumedBudget, " - + " COALESCE(p.totalManhour, 0) as plannedManhour, " - + " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) as actualConsumedManhour, " - + " sum(t.consumedBudget) / ((p.expectedTotalFee - ifnull(p.subContractFee, 0)) * 0.8) as budgetConsumptionRate, " - + " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) / COALESCE(p.totalManhour, 0) as manhourConsumptionRate, " - + " case " - + " when sum(t.consumedBudget) / ((p.expectedTotalFee - ifnull(p.subContractFee, 0)) * 0.8) >= 1 " - + " or (sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) / COALESCE(p.totalManhour, 0)) >= 1 " - + " then 'Overconsumption' " - + " when sum(t.consumedBudget) / ((p.expectedTotalFee - ifnull(p.subContractFee, 0)) * 0.8) >= :lowerLimit and sum(t.consumedBudget) / ((p.expectedTotalFee - ifnull(p.subContractFee, 0)) * 0.8) <= 1 " - + " or (sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) / COALESCE(p.totalManhour, 0)) >= :lowerLimit and (sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) / COALESCE(p.totalManhour, 0)) <= 1 " - + " then 'Potential Overconsumption' " - + " else 'Within Budget' " - + " END as status " - + " from " - + " (SELECT t.*, (t.normalConsumed + COALESCE(t.otConsumed, 0)) * sal.hourlyRate as consumedBudget " - + " FROM " - + " ( " - + " SELECT t.id AS tid, max(se.date) AS max_date " - + " FROM timesheet t " - + " INNER JOIN salary_effective se ON se.staffId = t.staffId AND se.date <= t.recordDate " - + " GROUP BY t.id " - + " ) max_se " - + " LEFT JOIN timesheet t ON t.id = max_se.tid " - + " LEFT JOIN staff s on s.id = t.staffId " - + " inner join salary_effective se on se.date = max_se.max_date " - + " left join salary sal on se.salaryId = sal.salaryPoint) t " - + " left join project p on p.id = t.projectId " - + " left join team tm on p.teamLead = tm.teamLead " - + " left join customer c on c.id = p.customerId " - + " LEFT JOIN subsidiary ss on p.customerSubsidiaryId = ss.id " - + " WHERE p.deleted = false " - + " and p.status = 'On-going' " + " SELECT " + + " p.code, p.name, tm.code as team, concat(c.code, ' -',c.name) as client, COALESCE(concat(ss.code, ' -', ss.name), 'N/A') as subsidiary " + + " , (p.expectedTotalFee - ifnull(p.subContractFee, 0)) * 0.8 as plannedBudget, sum(t.consumedBudget) as actualConsumedBudget " + + " , COALESCE(p.totalManhour, 0) as plannedManhour, sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) as actualConsumedManhour " + + " , sum(t.consumedBudget) / ((p.expectedTotalFee - ifnull(p.subContractFee, 0)) * 0.8) as budgetConsumptionRate " + + " , sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) / COALESCE(p.totalManhour, 0) as manhourConsumptionRate " + + " , case when sum(t.consumedBudget) / ((p.expectedTotalFee - ifnull(p.subContractFee, 0)) * 0.8) >= 1 " + + " or (sum(t.normalConsumed + COALESCE(t.otConsumed,0)) / COALESCE(p.totalManhour,0)) >= 1 " + + " then'Overconsumption' when sum(t.consumedBudget)/ ((p.expectedTotalFee - ifnull(p.subContractFee,0)) * 0.8) >= :lowerLimit " + + " and sum(t.consumedBudget)/ ((p.expectedTotalFee - ifnull(p.subContractFee,0)) * 0.8) <= 1 " + + " or(sum(t.normalConsumed+COALESCE(t.otConsumed, 0))/ COALESCE(p.totalManhour, 0)) >= :lowerLimit " + + " and (sum(t.normalConsumed+COALESCE(t.otConsumed, 0))/ COALESCE(p.totalManhour, 0)) <= 1 " + + " then'Potential Overconsumption' else'Within Budget' END as status " + + " from " + + " ( " + + " SELECT t.*, (t.normalConsumed+COALESCE(t.otConsumed, 0)) * sal.hourlyRate as consumedBudget " + + " FROM timesheet t " + + " INNER JOIN salary_effective se ON se.staffId = t.staffId AND t.recordDate BETWEEN se.startDate AND se.endDate " + + " LEFT JOIN salary sal on se.salaryId = sal.salaryPoint " + + " ) t " + + " left join project p on p.id = t.projectId " + + " left join team tm on p.teamLead = tm.teamLead " + + " left join customer c on c.id = p.customerId " + + " left join subsidiary ss on p.customerSubsidiaryId = ss.id " + + " WHERE p.deleted = false and p.status ='On-going' " ) var statusFilter: String = "" if (args != null) { @@ -3699,21 +3685,38 @@ open class ReportService( rowIndex = 3 sortedTeams.forEach { team: Team -> // not his/her team staffs - val staffs = timesheets + var staffs = timesheets .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 == team.id && it.project?.teamLead?.team?.id != it.staff?.team?.id } + var tempTimesheets = timesheets + .filter { + it.project?.teamLead?.team?.id == team.id + && it.project?.teamLead?.team?.id != it.staff?.team?.id + } + + if (teamId.lowercase() != "all" && teamId.toLong() != team.id) { + staffs = staffs.filter { + it.team.id == teamId.toLong() + } + + tempTimesheets = tempTimesheets.filter { + it.staff?.team?.id == teamId.toLong() + } + } + + val projects = tempTimesheets .mapNotNull { it.project } .sortedByDescending { it.code } .distinct() // Team - if (projects.isNotEmpty()) { + if (projects.isNotEmpty() && staffs.isNotEmpty()) { sheet.createRow(rowIndex++).apply { createCell(0).apply { setCellValue("Team to be charged:") @@ -3787,7 +3790,7 @@ open class ReportService( val startRow = rowIndex + 1 var endRow = rowIndex projects.forEach { project: Project -> - if (teamId.lowercase() == "all" || teamId.toLong() == project.teamLead?.team?.id || teamId.toLong() == team.id) { + if (teamId.lowercase() == "all" || teamId.toLong() == project.teamLead?.team?.id || teamId.toLong() == team.id || team.id == project.teamLead?.team?.id) { // if (team.id == project.teamLead?.team?.id) { endRow++ sheet.createRow(rowIndex++).apply { @@ -3836,7 +3839,7 @@ open class ReportService( } createCell(columnIndex).apply { - setCellValue(totalSalary) + setCellValue(totalSalary * chargeFee) val cloneStyle = workbook.createCellStyle() cloneStyle.cloneStyleFrom(boldFontWithBorderStyle) cellStyle = cloneStyle.apply { diff --git a/src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt b/src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt index 9179bd4..0c915be 100644 --- a/src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt +++ b/src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt @@ -172,7 +172,6 @@ class ReportController( val args: MutableMap = mutableMapOf( "status" to request.status, "lowerLimit" to lowerLimit - ) if (request.teamId != null) { args["teamId"] = request.teamId 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 d93c531..b862ab1 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 @@ -288,7 +288,7 @@ open class TimesheetsService( logger.info("---------project task-------") val projectTask = null - val nonBillableTask = taskRepository.findById(44).orElseThrow() + val nonBillableTask = taskRepository.findById(42).orElseThrow() // process record date logger.info("---------record date-------") val formatter = DateTimeFormatter.ofPattern("dd-MMM-yyyy") diff --git a/src/main/resources/db/changelog/changes/20240814_01_cyril/01_update_timesheet_and_task.sql b/src/main/resources/db/changelog/changes/20240814_01_cyril/01_update_timesheet_and_task.sql new file mode 100644 index 0000000..08b15d0 --- /dev/null +++ b/src/main/resources/db/changelog/changes/20240814_01_cyril/01_update_timesheet_and_task.sql @@ -0,0 +1,24 @@ +-- liquibase formatted sql +-- changeset cyril:timesheet,project_task,task_template_tasks,task + +UPDATE timesheet t, project_task pt1, project_task pt2 +SET t.projectTaskId = pt2.id +WHERE t.projectTaskId = pt1.id and t.projectId = pt2.project_id and pt2.task_id = 44 and (pt1.task_id = 42 or pt1.task_id = 43); + +UPDATE timesheet SET nonBillableTaskId = 42 where nonBillableTaskId in (43, 44); + +DELETE FROM project_task WHERE task_id in (42, 43); + +UPDATE project_task SET task_id = 42 WHERE task_id = 44; + +DELETE FROM task_template_tasks WHERE tasksId in (42, 43); + +UPDATE task_template_tasks SET tasksId = 42 WHERE tasksId = 44; + +DELETE FROM task +WHERE id=43; +DELETE FROM task +WHERE id=44; +UPDATE task +SET name='5.9 Others and remain fast time input' +WHERE id=42; \ No newline at end of file diff --git a/src/main/resources/db/changelog/changes/20240815_01_cyril/01_update_project.sql b/src/main/resources/db/changelog/changes/20240815_01_cyril/01_update_project.sql new file mode 100644 index 0000000..89d7ec9 --- /dev/null +++ b/src/main/resources/db/changelog/changes/20240815_01_cyril/01_update_project.sql @@ -0,0 +1,5 @@ +-- liquibase formatted sql +-- changeset cyril:project + +ALTER TABLE `project` + ADD COLUMN `ratePerManhour` DOUBLE NULL DEFAULT NULL AFTER `totalManhour`;