# Conflicts: # src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt # src/main/java/com/ffii/tsms/modules/report/web/ReportController.kttags/Baseline_30082024_BACKEND_UAT
| @@ -17,7 +17,7 @@ class CustomerContactService( | |||||
| } | } | ||||
| fun findByContactId(contactId: Long): CustomerContact { | fun findByContactId(contactId: Long): CustomerContact { | ||||
| return customerContactRepository.findById(contactId).orElseThrow() | |||||
| return customerContactRepository.findById(contactId).orElse(null) | |||||
| } | } | ||||
| fun saveContactsByCustomer(saveCustomerId: Long, saveCustomerContact: List<CustomerContact>) { | fun saveContactsByCustomer(saveCustomerId: Long, saveCustomerContact: List<CustomerContact>) { | ||||
| @@ -178,7 +178,7 @@ open class StaffsService( | |||||
| val company = companyRepository.findById(req.companyId).orElseThrow() | val company = companyRepository.findById(req.companyId).orElseThrow() | ||||
| val grade = if (req.gradeId != null && req.gradeId > 0L) gradeRepository.findById(req.gradeId).orElseThrow() else null | val grade = if (req.gradeId != null && req.gradeId > 0L) gradeRepository.findById(req.gradeId).orElseThrow() else null | ||||
| val team = if (req.teamId != null && req.teamId > 0L) teamRepository.findById(req.teamId).orElseThrow() else null | val team = if (req.teamId != null && req.teamId > 0L) teamRepository.findById(req.teamId).orElseThrow() else null | ||||
| val salary = salaryRepository.findById(req.salaryId).orElseThrow() | |||||
| val salary = salaryRepository.findBySalaryPoint(req.salaryId).orElseThrow() | |||||
| val department = departmentRepository.findById(req.departmentId).orElseThrow() | val department = departmentRepository.findById(req.departmentId).orElseThrow() | ||||
| staff.apply { | staff.apply { | ||||
| @@ -123,4 +123,12 @@ open class Project : BaseEntity<Long>() { | |||||
| // @JsonBackReference | // @JsonBackReference | ||||
| @JoinColumn(name = "mainProjectId") | @JoinColumn(name = "mainProjectId") | ||||
| open var mainProject: Project? = null | open var mainProject: Project? = null | ||||
| @ManyToOne | |||||
| @JoinColumn(name = "customerContactId") | |||||
| open var customerContact: CustomerContact? = null | |||||
| @ManyToOne | |||||
| @JoinColumn(name = "subsidiaryContactId") | |||||
| open var subsidiaryContact: SubsidiaryContact? = null | |||||
| } | } | ||||
| @@ -21,12 +21,12 @@ interface ProjectRepository : AbstractRepository<Project, Long> { | |||||
| fun findAllByPlanStartLessThanEqualAndPlanEndGreaterThanEqual(remainedDateFrom: LocalDate?, remainedDateTo: LocalDate?):List<Project> | fun findAllByPlanStartLessThanEqualAndPlanEndGreaterThanEqual(remainedDateFrom: LocalDate?, remainedDateTo: LocalDate?):List<Project> | ||||
| //fun findAllByDateRange(start: LocalDate, end: LocalDate): List<Project> | |||||
| @Query("SELECT max(cast(substring_index(substring_index(p.code, '-', 2), '-', -1) as long)) FROM Project p WHERE p.isClpProject = ?1 and p.id != ?2" + | @Query("SELECT max(cast(substring_index(substring_index(p.code, '-', 2), '-', -1) as long)) FROM Project p WHERE p.isClpProject = ?1 and p.id != ?2" + | ||||
| "") | "") | ||||
| fun getLatestCodeNumberByMainProject(isClpProject: Boolean, id: Serializable?): Long? | fun getLatestCodeNumberByMainProject(isClpProject: Boolean, id: Serializable?): Long? | ||||
| @Query("SELECT max(cast(substring_index(p.code, '-', -1) as long)) FROM Project p WHERE p.code like ?1 and p.id != ?2") | |||||
| @Query("SELECT max(case when length(p.code) - length(replace(p.code, '-', '')) > 1 then cast(substring_index(p.code, '-', -1) as long) end) FROM Project p WHERE p.code like ?1 and p.id != ?2") | |||||
| fun getLatestCodeNumberBySubProject(code: String, id: Serializable?): Long? | fun getLatestCodeNumberBySubProject(code: String, id: Serializable?): Long? | ||||
| fun findAllByStatusIsNotAndMainProjectIsNull(status: String): List<Project> | |||||
| } | } | ||||
| @@ -0,0 +1,15 @@ | |||||
| package com.ffii.tsms.modules.project.entity.projections | |||||
| data class ProjectResourceReport ( | |||||
| val code: String, | |||||
| val name: String, | |||||
| val team: String, | |||||
| val client: String, | |||||
| val plannedBudget: Double, | |||||
| val actualConsumedBudget: Double, | |||||
| val plannedManhour: Double, | |||||
| val actualConsumedManhour: Double, | |||||
| val budgetConsumptionRate: Double, | |||||
| val manhourConsumptionRate: Double, | |||||
| val status: String | |||||
| ) | |||||
| @@ -11,6 +11,9 @@ interface ProjectSearchInfo { | |||||
| val code: String? | val code: String? | ||||
| val status: String? | val status: String? | ||||
| @get:Value("#{target.mainProject?.code}") | |||||
| val mainProject: String? | |||||
| @get:Value("#{target.projectCategory.name}") | @get:Value("#{target.projectCategory.name}") | ||||
| val category: String? | val category: String? | ||||
| @@ -6,6 +6,7 @@ import com.ffii.tsms.modules.data.service.CustomerContactService | |||||
| import com.ffii.tsms.modules.project.entity.projections.ProjectSearchInfo | import com.ffii.tsms.modules.project.entity.projections.ProjectSearchInfo | ||||
| import com.ffii.tsms.modules.data.service.CustomerService | import com.ffii.tsms.modules.data.service.CustomerService | ||||
| import com.ffii.tsms.modules.data.service.GradeService | import com.ffii.tsms.modules.data.service.GradeService | ||||
| import com.ffii.tsms.modules.data.service.SubsidiaryContactService | |||||
| import com.ffii.tsms.modules.project.entity.* | import com.ffii.tsms.modules.project.entity.* | ||||
| import com.ffii.tsms.modules.project.entity.Milestone | import com.ffii.tsms.modules.project.entity.Milestone | ||||
| import com.ffii.tsms.modules.project.entity.projections.InvoiceInfoSearchInfo | import com.ffii.tsms.modules.project.entity.projections.InvoiceInfoSearchInfo | ||||
| @@ -44,10 +45,12 @@ open class ProjectsService( | |||||
| private val taskGroupRepository: TaskGroupRepository, | private val taskGroupRepository: TaskGroupRepository, | ||||
| private val timesheetRepository: TimesheetRepository, | private val timesheetRepository: TimesheetRepository, | ||||
| private val taskTemplateRepository: TaskTemplateRepository, | private val taskTemplateRepository: TaskTemplateRepository, | ||||
| private val subsidiaryRepository: SubsidiaryRepository, private val subsidiaryContactRepository: SubsidiaryContactRepository | |||||
| private val subsidiaryContactService: SubsidiaryContactService, | |||||
| private val subsidiaryContactRepository: SubsidiaryContactRepository | |||||
| ) { | ) { | ||||
| open fun allProjects(): List<ProjectSearchInfo> { | open fun allProjects(): List<ProjectSearchInfo> { | ||||
| return projectRepository.findProjectSearchInfoByOrderByCreatedDesc().sortedByDescending { it.status?.lowercase() != "deleted" } | |||||
| return projectRepository.findProjectSearchInfoByOrderByCreatedDesc() | |||||
| .sortedByDescending { it.status?.lowercase() != "deleted" } | |||||
| } | } | ||||
| open fun allInvoices(): List<InvoiceSearchInfo> { | open fun allInvoices(): List<InvoiceSearchInfo> { | ||||
| @@ -63,63 +66,57 @@ open class ProjectsService( | |||||
| } | } | ||||
| open fun markDeleted(id: Long) { | open fun markDeleted(id: Long) { | ||||
| projectRepository.save(projectRepository.findById(id).orElseThrow() | |||||
| .apply { | |||||
| deleted = true | |||||
| status = "Deleted" | |||||
| }) | |||||
| projectRepository.save(projectRepository.findById(id).orElseThrow().apply { | |||||
| deleted = true | |||||
| status = "Deleted" | |||||
| }) | |||||
| } | } | ||||
| open fun allAssignedProjects(): List<AssignedProject> { | open fun allAssignedProjects(): List<AssignedProject> { | ||||
| return SecurityUtils.getUser().getOrNull()?.let { user -> | return SecurityUtils.getUser().getOrNull()?.let { user -> | ||||
| staffRepository.findByUserId(user.id).getOrNull()?.let { staff -> | staffRepository.findByUserId(user.id).getOrNull()?.let { staff -> | ||||
| staffAllocationRepository.findAssignedProjectsByStaff(staff) | |||||
| .mapNotNull { | |||||
| it.project?.let { project -> | |||||
| val timesheetHours = timesheetRepository.totalHoursConsumedByProject(project) | |||||
| AssignedProject( | |||||
| id = project.id!!, | |||||
| code = project.code!!, | |||||
| name = project.name!!, | |||||
| tasks = projectTaskRepository.findAllByProject(project).mapNotNull { pt -> pt.task }, | |||||
| milestones = milestoneRepository.findAllByProject(project) | |||||
| .filter { milestone -> milestone.taskGroup?.id != null } | |||||
| .associateBy { milestone -> milestone.taskGroup!!.id!! } | |||||
| .mapValues { (_, milestone) -> | |||||
| MilestoneInfo( | |||||
| startDate = milestone.startDate?.format(DateTimeFormatter.ISO_LOCAL_DATE), | |||||
| endDate = milestone.endDate?.format(DateTimeFormatter.ISO_LOCAL_DATE) | |||||
| ) | |||||
| }, | |||||
| hoursAllocated = project.totalManhour ?: 0.0, | |||||
| hoursAllocatedOther = 0.0, | |||||
| hoursSpent = timesheetHours.normalConsumed, | |||||
| hoursSpentOther = timesheetHours.otConsumed | |||||
| ) | |||||
| } | |||||
| staffAllocationRepository.findAssignedProjectsByStaff(staff).mapNotNull { | |||||
| it.project?.let { project -> | |||||
| val timesheetHours = timesheetRepository.totalHoursConsumedByProject(project) | |||||
| AssignedProject(id = project.id!!, | |||||
| code = project.code!!, | |||||
| name = project.name!!, | |||||
| tasks = projectTaskRepository.findAllByProject(project).mapNotNull { pt -> pt.task }, | |||||
| milestones = milestoneRepository.findAllByProject(project) | |||||
| .filter { milestone -> milestone.taskGroup?.id != null } | |||||
| .associateBy { milestone -> milestone.taskGroup!!.id!! } | |||||
| .mapValues { (_, milestone) -> | |||||
| MilestoneInfo( | |||||
| startDate = milestone.startDate?.format(DateTimeFormatter.ISO_LOCAL_DATE), | |||||
| endDate = milestone.endDate?.format(DateTimeFormatter.ISO_LOCAL_DATE) | |||||
| ) | |||||
| }, | |||||
| hoursAllocated = project.totalManhour ?: 0.0, | |||||
| hoursAllocatedOther = 0.0, | |||||
| hoursSpent = timesheetHours.normalConsumed, | |||||
| hoursSpentOther = timesheetHours.otConsumed | |||||
| ) | |||||
| } | } | ||||
| } | |||||
| } | } | ||||
| } ?: emptyList() | } ?: emptyList() | ||||
| } | } | ||||
| open fun allProjectWithTasks(): List<ProjectWithTasks> { | open fun allProjectWithTasks(): List<ProjectWithTasks> { | ||||
| return projectRepository.findAll().map { project -> | return projectRepository.findAll().map { project -> | ||||
| ProjectWithTasks( | |||||
| id = project.id!!, | |||||
| ProjectWithTasks(id = project.id!!, | |||||
| code = project.code!!, | code = project.code!!, | ||||
| name = project.name!!, | name = project.name!!, | ||||
| tasks = projectTaskRepository.findAllByProject(project).mapNotNull { pt -> pt.task }, | tasks = projectTaskRepository.findAllByProject(project).mapNotNull { pt -> pt.task }, | ||||
| milestones = milestoneRepository.findAllByProject(project) | milestones = milestoneRepository.findAllByProject(project) | ||||
| .filter { milestone -> milestone.taskGroup?.id != null } | .filter { milestone -> milestone.taskGroup?.id != null } | ||||
| .associateBy { milestone -> milestone.taskGroup!!.id!! } | |||||
| .mapValues { (_, milestone) -> | |||||
| .associateBy { milestone -> milestone.taskGroup!!.id!! }.mapValues { (_, milestone) -> | |||||
| MilestoneInfo( | MilestoneInfo( | ||||
| startDate = milestone.startDate?.format(DateTimeFormatter.ISO_LOCAL_DATE), | startDate = milestone.startDate?.format(DateTimeFormatter.ISO_LOCAL_DATE), | ||||
| endDate = milestone.endDate?.format(DateTimeFormatter.ISO_LOCAL_DATE) | endDate = milestone.endDate?.format(DateTimeFormatter.ISO_LOCAL_DATE) | ||||
| ) | ) | ||||
| } | |||||
| ) | |||||
| }) | |||||
| } | } | ||||
| } | } | ||||
| @@ -135,15 +132,28 @@ open class ProjectsService( | |||||
| if (latestProjectCode != null) { | if (latestProjectCode != null) { | ||||
| val lastFix = latestProjectCode | val lastFix = latestProjectCode | ||||
| return "$prefix-" + String.format("%04d", lastFix + 1L) | |||||
| return "$prefix-" + String.format("%04d", lastFix + 1L) | |||||
| } else { | } else { | ||||
| return "$prefix-0001" | return "$prefix-0001" | ||||
| } | } | ||||
| } | } | ||||
| open fun createSubProjectCode(mainProject: Project, project: Project): String { | |||||
| val prefix = mainProject.code | |||||
| val latestProjectCode = projectRepository.getLatestCodeNumberBySubProject(prefix!!, project.id ?: -1) | |||||
| if (latestProjectCode != null) { | |||||
| val lastFix = latestProjectCode | |||||
| return "$prefix-" + String.format("%03d", lastFix + 1L) | |||||
| } else { | |||||
| return "$prefix-001" | |||||
| } | |||||
| } | |||||
| @Transactional | @Transactional | ||||
| open fun saveProject(request: NewProjectRequest): NewProjectResponse { | open fun saveProject(request: NewProjectRequest): NewProjectResponse { | ||||
| val projectCategory = | |||||
| projectCategoryRepository.findById(request.projectCategoryId).orElseThrow() | |||||
| val projectCategory = projectCategoryRepository.findById(request.projectCategoryId).orElseThrow() | |||||
| val fundingType = fundingTypeRepository.findById(request.fundingTypeId).orElseThrow() | val fundingType = fundingTypeRepository.findById(request.fundingTypeId).orElseThrow() | ||||
| val serviceType = serviceTypeRepository.findById(request.serviceTypeId).orElseThrow() | val serviceType = serviceTypeRepository.findById(request.serviceTypeId).orElseThrow() | ||||
| val contractType = contractTypeRepository.findById(request.contractTypeId).orElseThrow() | val contractType = contractTypeRepository.findById(request.contractTypeId).orElseThrow() | ||||
| @@ -158,6 +168,7 @@ open class ProjectsService( | |||||
| val subsidiaryContact = subsidiaryContactRepository.findById(request.clientContactId).orElse(null) | val subsidiaryContact = subsidiaryContactRepository.findById(request.clientContactId).orElse(null) | ||||
| val clientContact = customerContactService.findByContactId(request.clientContactId) | val clientContact = customerContactService.findByContactId(request.clientContactId) | ||||
| val customerSubsidiary = request.clientSubsidiaryId?.let { subsidiaryService.findSubsidiary(it) } | val customerSubsidiary = request.clientSubsidiaryId?.let { subsidiaryService.findSubsidiary(it) } | ||||
| val mainProject = if (request.mainProjectId != null && request.mainProjectId > 0) projectRepository.findById(request.mainProjectId).orElse(null) else null | |||||
| val allTasksMap = tasksService.allTasks().associateBy { it.id } | val allTasksMap = tasksService.allTasks().associateBy { it.id } | ||||
| val taskGroupMap = tasksService.allTaskGroups().associateBy { it.id } | val taskGroupMap = tasksService.allTaskGroups().associateBy { it.id } | ||||
| @@ -169,7 +180,7 @@ open class ProjectsService( | |||||
| project.apply { | project.apply { | ||||
| name = request.projectName | name = request.projectName | ||||
| description = request.projectDescription | description = request.projectDescription | ||||
| code = if (this.code.isNullOrEmpty()) createProjectCode(request.isClpProject, project) else this.code | |||||
| code = if (this.code.isNullOrEmpty() && request.mainProjectId == null) createProjectCode(request.isClpProject, project) else if (this.code.isNullOrEmpty() && request.mainProjectId != null && mainProject != null) createSubProjectCode(mainProject, project) else this.code | |||||
| expectedTotalFee = request.expectedProjectFee | expectedTotalFee = request.expectedProjectFee | ||||
| totalManhour = request.totalManhour | totalManhour = request.totalManhour | ||||
| actualStart = request.projectActualStart | actualStart = request.projectActualStart | ||||
| @@ -179,6 +190,7 @@ open class ProjectsService( | |||||
| else if (this.actualStart != null) "On-going" | else if (this.actualStart != null) "On-going" | ||||
| else "Pending To Start" | else "Pending To Start" | ||||
| isClpProject = request.isClpProject | isClpProject = request.isClpProject | ||||
| this.mainProject = mainProject | |||||
| this.projectCategory = projectCategory | this.projectCategory = projectCategory | ||||
| this.fundingType = fundingType | this.fundingType = fundingType | ||||
| @@ -191,10 +203,15 @@ open class ProjectsService( | |||||
| this.teamLead = teamLead | this.teamLead = teamLead | ||||
| this.customer = customer | this.customer = customer | ||||
| custLeadName = if (request.isSubsidiaryContact == null || !request.isSubsidiaryContact) clientContact.name else subsidiaryContact.name | |||||
| custLeadEmail = if (request.isSubsidiaryContact == null || !request.isSubsidiaryContact) clientContact.email else subsidiaryContact.email | |||||
| custLeadPhone = if (request.isSubsidiaryContact == null || !request.isSubsidiaryContact) clientContact.phone else subsidiaryContact.phone | |||||
| custLeadName = | |||||
| if (request.isSubsidiaryContact == null || !request.isSubsidiaryContact) clientContact.name else subsidiaryContact.name | |||||
| custLeadEmail = | |||||
| if (request.isSubsidiaryContact == null || !request.isSubsidiaryContact) clientContact.email else subsidiaryContact.email | |||||
| custLeadPhone = | |||||
| if (request.isSubsidiaryContact == null || !request.isSubsidiaryContact) clientContact.phone else subsidiaryContact.phone | |||||
| this.customerSubsidiary = customerSubsidiary | this.customerSubsidiary = customerSubsidiary | ||||
| this.customerContact = if (customerSubsidiary == null) clientContact else null | |||||
| this.subsidiaryContact = if (customerSubsidiary != null) subsidiaryContact else null | |||||
| } | } | ||||
| @@ -203,8 +220,7 @@ open class ProjectsService( | |||||
| val milestones = request.taskGroups.entries.map { (taskStageId, taskGroupAllocation) -> | val milestones = request.taskGroups.entries.map { (taskStageId, taskGroupAllocation) -> | ||||
| val taskGroup = taskGroupRepository.findById(taskStageId).orElse(TaskGroup()) ?: TaskGroup() | val taskGroup = taskGroupRepository.findById(taskStageId).orElse(TaskGroup()) ?: TaskGroup() | ||||
| val milestone = if (project.id != null && project.id!! > 0L) milestoneRepository.findByProjectAndTaskGroup( | val milestone = if (project.id != null && project.id!! > 0L) milestoneRepository.findByProjectAndTaskGroup( | ||||
| project, | |||||
| taskGroup | |||||
| project, taskGroup | |||||
| ) ?: Milestone() else Milestone() | ) ?: Milestone() else Milestone() | ||||
| milestone.apply { | milestone.apply { | ||||
| val newMilestone = this | val newMilestone = this | ||||
| @@ -233,8 +249,7 @@ open class ProjectsService( | |||||
| taskGroupAllocation.taskIds.map { taskId -> | taskGroupAllocation.taskIds.map { taskId -> | ||||
| val projectTask = | val projectTask = | ||||
| if (project.id != null && project.id!! > 0L) projectTaskRepository.findByProjectAndTask( | if (project.id != null && project.id!! > 0L) projectTaskRepository.findByProjectAndTask( | ||||
| project, | |||||
| allTasksMap[taskId]!! | |||||
| project, allTasksMap[taskId]!! | |||||
| ) ?: ProjectTask() else ProjectTask() | ) ?: ProjectTask() else ProjectTask() | ||||
| projectTask.apply { | projectTask.apply { | ||||
| @@ -252,8 +267,7 @@ open class ProjectsService( | |||||
| val gradeAllocations = request.manhourPercentageByGrade.entries.map { | val gradeAllocations = request.manhourPercentageByGrade.entries.map { | ||||
| val gradeAllocation = | val gradeAllocation = | ||||
| if (project.id != null && project.id!! > 0L) gradeAllocationRepository.findByProjectAndGrade( | if (project.id != null && project.id!! > 0L) gradeAllocationRepository.findByProjectAndGrade( | ||||
| project, | |||||
| gradeMap[it.key]!! | |||||
| project, gradeMap[it.key]!! | |||||
| ) ?: GradeAllocation() else GradeAllocation() | ) ?: GradeAllocation() else GradeAllocation() | ||||
| gradeAllocation.apply { | gradeAllocation.apply { | ||||
| @@ -313,16 +327,16 @@ open class ProjectsService( | |||||
| val project = projectRepository.findById(projectId) | val project = projectRepository.findById(projectId) | ||||
| return project.getOrNull()?.let { | return project.getOrNull()?.let { | ||||
| val customerContact = | |||||
| it.customer?.id?.let { customerId -> customerContactService.findAllByCustomerId(customerId) } | |||||
| ?: emptyList() | |||||
| // val subsidiaryContact = it.customerSubsidiary?.id?.let { subsidiaryId -> | |||||
| // subsidiaryContactService.findAllBySubsidiaryId(subsidiaryId) | |||||
| // } ?: emptyList() | |||||
| // val customerContact = it.customer?.id?.let { customerId -> customerContactService.findAllByCustomerId(customerId) } | |||||
| // ?: emptyList() | |||||
| val milestoneMap = it.milestones | |||||
| .filter { milestone -> milestone.taskGroup?.id != null } | |||||
| val milestoneMap = it.milestones.filter { milestone -> milestone.taskGroup?.id != null } | |||||
| .associateBy { milestone -> milestone.taskGroup!!.id!! } | .associateBy { milestone -> milestone.taskGroup!!.id!! } | ||||
| EditProjectDetails( | |||||
| projectId = it.id, | |||||
| EditProjectDetails(projectId = it.id, | |||||
| projectDeleted = it.deleted, | projectDeleted = it.deleted, | ||||
| projectCode = it.code, | projectCode = it.code, | ||||
| projectName = it.name, | projectName = it.name, | ||||
| @@ -333,6 +347,7 @@ open class ProjectsService( | |||||
| projectActualEnd = it.actualEnd, | projectActualEnd = it.actualEnd, | ||||
| projectStatus = it.status, | projectStatus = it.status, | ||||
| isClpProject = it.isClpProject, | isClpProject = it.isClpProject, | ||||
| mainProjectId = it.mainProject?.id, | |||||
| serviceTypeId = it.serviceType?.id, | serviceTypeId = it.serviceType?.id, | ||||
| fundingTypeId = it.fundingType?.id, | fundingTypeId = it.fundingType?.id, | ||||
| contractTypeId = it.contractType?.id, | contractTypeId = it.contractType?.id, | ||||
| @@ -341,7 +356,8 @@ open class ProjectsService( | |||||
| buildingTypeIds = it.buildingTypes.mapNotNull { buildingType -> buildingType.id }, | buildingTypeIds = it.buildingTypes.mapNotNull { buildingType -> buildingType.id }, | ||||
| workNatureIds = it.workNatures.mapNotNull { workNature -> workNature.id }, | workNatureIds = it.workNatures.mapNotNull { workNature -> workNature.id }, | ||||
| clientId = it.customer?.id, | clientId = it.customer?.id, | ||||
| clientContactId = customerContact.find { contact -> contact.name == it.custLeadName }?.id, | |||||
| clientContactId = it.subsidiaryContact?.id ?: it.customerContact?.id, | |||||
| // subsidiaryContact.find { contact -> contact.name == it.custLeadName }?.id ?: customerContact.find { contact -> contact.name == it.custLeadName }?.id, | |||||
| clientSubsidiaryId = it.customerSubsidiary?.id, | clientSubsidiaryId = it.customerSubsidiary?.id, | ||||
| totalManhour = it.totalManhour, | totalManhour = it.totalManhour, | ||||
| manhourPercentageByGrade = gradeAllocationRepository.findByProject(it) | manhourPercentageByGrade = gradeAllocationRepository.findByProject(it) | ||||
| @@ -349,8 +365,7 @@ open class ProjectsService( | |||||
| .associate { allocation -> Pair(allocation.grade!!.id!!, allocation.manhour ?: 0.0) }, | .associate { allocation -> Pair(allocation.grade!!.id!!, allocation.manhour ?: 0.0) }, | ||||
| taskGroups = projectTaskRepository.findAllByProject(it) | taskGroups = projectTaskRepository.findAllByProject(it) | ||||
| .mapNotNull { projectTask -> if (projectTask.task?.taskGroup?.id != null) projectTask.task else null } | .mapNotNull { projectTask -> if (projectTask.task?.taskGroup?.id != null) projectTask.task else null } | ||||
| .groupBy { task -> task.taskGroup!!.id!! } | |||||
| .mapValues { (taskGroupId, tasks) -> | |||||
| .groupBy { task -> task.taskGroup!!.id!! }.mapValues { (taskGroupId, tasks) -> | |||||
| TaskGroupAllocation( | TaskGroupAllocation( | ||||
| taskIds = tasks.mapNotNull { task -> task.id }, | taskIds = tasks.mapNotNull { task -> task.id }, | ||||
| percentAllocation = milestoneMap[taskGroupId]?.stagePercentAllocation ?: 0.0 | percentAllocation = milestoneMap[taskGroupId]?.stagePercentAllocation ?: 0.0 | ||||
| @@ -359,8 +374,9 @@ open class ProjectsService( | |||||
| allocatedStaffIds = staffAllocationRepository.findByProject(it) | allocatedStaffIds = staffAllocationRepository.findByProject(it) | ||||
| .mapNotNull { allocation -> allocation.staff?.id }, | .mapNotNull { allocation -> allocation.staff?.id }, | ||||
| milestones = milestoneMap.mapValues { (_, milestone) -> | milestones = milestoneMap.mapValues { (_, milestone) -> | ||||
| com.ffii.tsms.modules.project.web.models.Milestone( | |||||
| startDate = milestone.startDate?.format(DateTimeFormatter.ISO_LOCAL_DATE), | |||||
| com.ffii.tsms.modules.project.web.models.Milestone(startDate = milestone.startDate?.format( | |||||
| DateTimeFormatter.ISO_LOCAL_DATE | |||||
| ), | |||||
| endDate = milestone.endDate?.format(DateTimeFormatter.ISO_LOCAL_DATE), | endDate = milestone.endDate?.format(DateTimeFormatter.ISO_LOCAL_DATE), | ||||
| payments = milestone.milestonePayments.map { payment -> | payments = milestone.milestonePayments.map { payment -> | ||||
| PaymentInputs( | PaymentInputs( | ||||
| @@ -369,8 +385,7 @@ open class ProjectsService( | |||||
| description = payment.description!!, | description = payment.description!!, | ||||
| date = payment.date!!.format(DateTimeFormatter.ISO_LOCAL_DATE) | date = payment.date!!.format(DateTimeFormatter.ISO_LOCAL_DATE) | ||||
| ) | ) | ||||
| } | |||||
| ) | |||||
| }) | |||||
| }, | }, | ||||
| expectedProjectFee = it.expectedTotalFee | expectedProjectFee = it.expectedTotalFee | ||||
| ) | ) | ||||
| @@ -400,4 +415,37 @@ open class ProjectsService( | |||||
| open fun allWorkNatures(): List<WorkNature> { | open fun allWorkNatures(): List<WorkNature> { | ||||
| return workNatureRepository.findAll() | return workNatureRepository.findAll() | ||||
| } | } | ||||
| open fun allMainProjects(): List<MainProjectDetails> { | |||||
| val mainProjects: List<Project> = projectRepository.findAllByStatusIsNotAndMainProjectIsNull("Deleted") | |||||
| return mainProjects.map { project: Project -> | |||||
| val subsidiaryContact = project.customerSubsidiary?.id?.let { subsidiaryId -> | |||||
| subsidiaryContactService.findAllBySubsidiaryId(subsidiaryId) | |||||
| } ?: emptyList() | |||||
| val customerContact = project.customer?.id?.let { customerId -> customerContactService.findAllByCustomerId(customerId) } | |||||
| ?: emptyList() | |||||
| MainProjectDetails( | |||||
| projectId = project.id, | |||||
| projectCode = project.code, | |||||
| projectName = project.name, | |||||
| projectCategoryId = project.projectCategory?.id, | |||||
| projectDescription = project.description, | |||||
| projectLeadId = project.teamLead?.id, | |||||
| projectStatus = project.status, | |||||
| isClpProject = project.isClpProject, | |||||
| serviceTypeId = project.serviceType?.id, | |||||
| fundingTypeId = project.fundingType?.id, | |||||
| contractTypeId = project.contractType?.id, | |||||
| locationId = project.location?.id, | |||||
| buildingTypeIds = project.buildingTypes.mapNotNull { buildingType -> buildingType.id }, | |||||
| workNatureIds = project.workNatures.mapNotNull { workNature -> workNature.id }, | |||||
| clientId = project.customer?.id, | |||||
| clientContactId = subsidiaryContact.find { contact -> contact.name == project.custLeadName }?.id ?: customerContact.find { contact -> contact.name == project.custLeadName }?.id, | |||||
| clientSubsidiaryId = project.customerSubsidiary?.id, | |||||
| expectedProjectFee = project.expectedTotalFee | |||||
| ) | |||||
| } | |||||
| } | |||||
| } | } | ||||
| @@ -2,8 +2,10 @@ package com.ffii.tsms.modules.project.web | |||||
| import com.ffii.core.exception.NotFoundException | import com.ffii.core.exception.NotFoundException | ||||
| import com.ffii.tsms.modules.data.entity.* | import com.ffii.tsms.modules.data.entity.* | ||||
| import com.ffii.tsms.modules.project.entity.Project | |||||
| import com.ffii.tsms.modules.project.entity.projections.ProjectSearchInfo | import com.ffii.tsms.modules.project.entity.projections.ProjectSearchInfo | ||||
| import com.ffii.tsms.modules.project.entity.ProjectCategory | import com.ffii.tsms.modules.project.entity.ProjectCategory | ||||
| import com.ffii.tsms.modules.project.entity.ProjectRepository | |||||
| import com.ffii.tsms.modules.project.service.ProjectsService | import com.ffii.tsms.modules.project.service.ProjectsService | ||||
| import com.ffii.tsms.modules.project.web.models.* | import com.ffii.tsms.modules.project.web.models.* | ||||
| import jakarta.validation.Valid | import jakarta.validation.Valid | ||||
| @@ -12,12 +14,17 @@ import org.springframework.web.bind.annotation.* | |||||
| @RestController | @RestController | ||||
| @RequestMapping("/projects") | @RequestMapping("/projects") | ||||
| class ProjectsController(private val projectsService: ProjectsService) { | |||||
| class ProjectsController(private val projectsService: ProjectsService, private val projectRepository: ProjectRepository) { | |||||
| @GetMapping | @GetMapping | ||||
| fun allProjects(): List<ProjectSearchInfo> { | fun allProjects(): List<ProjectSearchInfo> { | ||||
| return projectsService.allProjects() | return projectsService.allProjects() | ||||
| } | } | ||||
| @GetMapping("/main") | |||||
| fun allMainProjects(): List<MainProjectDetails> { | |||||
| return projectsService.allMainProjects() | |||||
| } | |||||
| @DeleteMapping("/{id}") | @DeleteMapping("/{id}") | ||||
| @ResponseStatus(HttpStatus.NO_CONTENT) | @ResponseStatus(HttpStatus.NO_CONTENT) | ||||
| fun deleteProject(@PathVariable id: Long) { | fun deleteProject(@PathVariable id: Long) { | ||||
| @@ -15,6 +15,7 @@ data class EditProjectDetails( | |||||
| val projectActualEnd: LocalDate?, | val projectActualEnd: LocalDate?, | ||||
| val projectStatus: String?, | val projectStatus: String?, | ||||
| val isClpProject: Boolean?, | val isClpProject: Boolean?, | ||||
| val mainProjectId: Long?, | |||||
| val serviceTypeId: Long?, | val serviceTypeId: Long?, | ||||
| val fundingTypeId: Long?, | val fundingTypeId: Long?, | ||||
| @@ -0,0 +1,30 @@ | |||||
| package com.ffii.tsms.modules.project.web.models | |||||
| data class MainProjectDetails ( | |||||
| // Project details | |||||
| val projectId: Long?, | |||||
| val projectCode: String?, | |||||
| val projectName: String?, | |||||
| val projectCategoryId: Long?, | |||||
| val projectDescription: String?, | |||||
| val projectLeadId: Long?, | |||||
| val projectStatus: String?, | |||||
| val isClpProject: Boolean?, | |||||
| val serviceTypeId: Long?, | |||||
| val fundingTypeId: Long?, | |||||
| val contractTypeId: Long?, | |||||
| val locationId: Long?, | |||||
| val buildingTypeIds: List<Long>, | |||||
| val workNatureIds: List<Long>, | |||||
| // Client details | |||||
| val clientId: Long?, | |||||
| val clientContactId: Long?, | |||||
| val clientSubsidiaryId: Long?, | |||||
| val expectedProjectFee: Double?, | |||||
| ) | |||||
| @@ -17,6 +17,7 @@ data class NewProjectRequest( | |||||
| val projectActualStart: LocalDate?, | val projectActualStart: LocalDate?, | ||||
| val projectActualEnd: LocalDate?, | val projectActualEnd: LocalDate?, | ||||
| val isClpProject: Boolean?, | val isClpProject: Boolean?, | ||||
| val mainProjectId: Long?, | |||||
| val serviceTypeId: Long, | val serviceTypeId: Long, | ||||
| val fundingTypeId: Long, | val fundingTypeId: Long, | ||||
| @@ -7,7 +7,12 @@ import com.ffii.tsms.modules.data.entity.Staff | |||||
| import com.ffii.tsms.modules.data.entity.Team | import com.ffii.tsms.modules.data.entity.Team | ||||
| import com.ffii.tsms.modules.project.entity.Invoice | import com.ffii.tsms.modules.project.entity.Invoice | ||||
| import com.ffii.tsms.modules.project.entity.Project | import com.ffii.tsms.modules.project.entity.Project | ||||
| import com.ffii.tsms.modules.timesheet.entity.Leave | |||||
| import com.ffii.tsms.modules.timesheet.entity.Timesheet | import com.ffii.tsms.modules.timesheet.entity.Timesheet | ||||
| import com.ffii.tsms.modules.timesheet.entity.projections.MonthlyLeave | |||||
| import com.ffii.tsms.modules.timesheet.entity.projections.ProjectMonthlyHoursWithDate | |||||
| import com.ffii.tsms.modules.timesheet.entity.projections.TimesheetHours | |||||
| import com.ffii.tsms.modules.timesheet.web.models.LeaveEntry | |||||
| import org.apache.commons.logging.Log | import org.apache.commons.logging.Log | ||||
| import org.apache.commons.logging.LogFactory | import org.apache.commons.logging.LogFactory | ||||
| import org.apache.poi.ss.usermodel.* | import org.apache.poi.ss.usermodel.* | ||||
| @@ -20,6 +25,7 @@ import org.springframework.stereotype.Service | |||||
| import java.io.ByteArrayOutputStream | import java.io.ByteArrayOutputStream | ||||
| import java.io.IOException | import java.io.IOException | ||||
| import java.math.BigDecimal | import java.math.BigDecimal | ||||
| import java.sql.Time | |||||
| import java.time.LocalDate | import java.time.LocalDate | ||||
| import java.time.YearMonth | import java.time.YearMonth | ||||
| import java.time.format.DateTimeFormatter | import java.time.format.DateTimeFormatter | ||||
| @@ -27,6 +33,8 @@ import java.util.* | |||||
| data class DayInfo(val date: String?, val weekday: String?) | data class DayInfo(val date: String?, val weekday: String?) | ||||
| @Service | @Service | ||||
| open class ReportService( | open class ReportService( | ||||
| private val jdbcDao: JdbcDao, | private val jdbcDao: JdbcDao, | ||||
| @@ -41,6 +49,7 @@ open class ReportService( | |||||
| private val MONTHLY_WORK_HOURS_ANALYSIS_REPORT = "templates/report/AR08_Monthly Work Hours Analysis Report.xlsx" | private val MONTHLY_WORK_HOURS_ANALYSIS_REPORT = "templates/report/AR08_Monthly Work Hours Analysis Report.xlsx" | ||||
| private val SALART_LIST_TEMPLATE = "templates/report/Salary Template.xlsx" | private val SALART_LIST_TEMPLATE = "templates/report/Salary Template.xlsx" | ||||
| private val LATE_START_REPORT = "templates/report/AR01_Late Start Report v01.xlsx" | private val LATE_START_REPORT = "templates/report/AR01_Late Start Report v01.xlsx" | ||||
| private val RESOURCE_OVERCONSUMPTION_REPORT = "templates/report/AR03_Resource Overconsumption.xlsx" | |||||
| // ==============================|| GENERATE REPORT ||============================== // | // ==============================|| GENERATE REPORT ||============================== // | ||||
| @@ -143,6 +152,29 @@ open class ReportService( | |||||
| return outputStream.toByteArray() | return outputStream.toByteArray() | ||||
| } | } | ||||
| @Throws(IOException::class) | |||||
| fun generateProjectResourceOverconsumptionReport( | |||||
| team: String, | |||||
| customer: String, | |||||
| result: List<Map<String, Any>>, | |||||
| lowerLimit: Double | |||||
| ): ByteArray { | |||||
| // Generate the Excel report with query results | |||||
| val workbook: Workbook = createProjectResourceOverconsumptionReport( | |||||
| team, | |||||
| customer, | |||||
| result, | |||||
| lowerLimit, | |||||
| RESOURCE_OVERCONSUMPTION_REPORT | |||||
| ) | |||||
| // Write the workbook to a ByteArrayOutputStream | |||||
| val outputStream: ByteArrayOutputStream = ByteArrayOutputStream() | |||||
| workbook.write(outputStream) | |||||
| workbook.close() | |||||
| return outputStream.toByteArray() | |||||
| } | |||||
| @Throws(IOException::class) | @Throws(IOException::class) | ||||
| fun exportSalaryList(salarys: List<Salary>): ByteArray { | fun exportSalaryList(salarys: List<Salary>): ByteArray { | ||||
| // Generate the Excel report with query results | // Generate the Excel report with query results | ||||
| @@ -733,10 +765,13 @@ open class ReportService( | |||||
| var projectList: List<String> = listOf() | var projectList: List<String> = listOf() | ||||
| println("----timesheets-----") | println("----timesheets-----") | ||||
| println(timesheets) | println(timesheets) | ||||
| println("----leaves-----") | |||||
| println(leaves) | |||||
| // result = timesheet record mapped | // result = timesheet record mapped | ||||
| var result: Map<String, Any> = mapOf() | var result: Map<String, Any> = mapOf() | ||||
| if (timesheets.isNotEmpty()) { | if (timesheets.isNotEmpty()) { | ||||
| projectList = timesheets.map{ "${it["code"]}\n ${it["name"]}"}.toList() | |||||
| projectList = timesheets.map{ "${it["code"]}\n ${it["name"]}"}.toList().distinct() | |||||
| result = timesheets.groupBy( | result = timesheets.groupBy( | ||||
| { it["id"].toString() }, | { it["id"].toString() }, | ||||
| { mapOf( | { mapOf( | ||||
| @@ -916,41 +951,40 @@ open class ReportService( | |||||
| columnSize++ | columnSize++ | ||||
| } | } | ||||
| println(columnSize) | println(columnSize) | ||||
| ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// | |||||
| /////////////////////////////////////////////////////////////// main data ////////////////////////////////////////////////////////////// | |||||
| if (timesheets.isNotEmpty()) { | if (timesheets.isNotEmpty()) { | ||||
| projectList.forEach { _ -> | |||||
| projectList.forEachIndexed { index, _ -> | |||||
| for (i in 0 until rowSize) { | for (i in 0 until rowSize) { | ||||
| tempCell = sheet.getRow(8 + i).createCell(columnIndex) | |||||
| tempCell = sheet.getRow(8 + i).createCell(columnIndex + index) | |||||
| tempCell.setCellValue(0.0) | tempCell.setCellValue(0.0) | ||||
| tempCell.cellStyle.dataFormat = accountingStyle | tempCell.cellStyle.dataFormat = accountingStyle | ||||
| } | } | ||||
| result.forEach{(id, list) -> | |||||
| for (i in 0 until id.toInt()) { | |||||
| val temp: List<Map<String, Any>> = list as List<Map<String, Any>> | |||||
| temp.forEachIndexed { i, _ -> | |||||
| dayInt = temp[i]["date"].toString().toInt() | |||||
| tempCell = sheet.getRow(dayInt.plus(7)).createCell(columnIndex) | |||||
| tempCell.setCellValue(temp[i]["normalConsumed"] as Double) | |||||
| } | |||||
| } | |||||
| result.forEach{ (id, list) -> | |||||
| val temp: List<Map<String, Any>> = list as List<Map<String, Any>> | |||||
| temp.forEachIndexed { index, _ -> | |||||
| dayInt = temp[index]["date"].toString().toInt() | |||||
| tempCell = sheet.getRow(dayInt.plus(7)).createCell(columnIndex) | |||||
| tempCell.setCellValue((temp[index]["normalConsumed"] as Double) + (temp[index]["otConsumed"] as Double)) | |||||
| } | } | ||||
| } | |||||
| columnIndex++ | columnIndex++ | ||||
| } | } | ||||
| } | } | ||||
| // leave hours data | |||||
| /////////////////////////////////////////////////////////////// leave hours data ////////////////////////////////////////////////////////////// | |||||
| for (i in 0 until rowSize) { | |||||
| tempCell = sheet.getRow(8 + i).createCell(columnIndex) | |||||
| tempCell.setCellValue(0.0) | |||||
| tempCell.cellStyle.dataFormat = accountingStyle | |||||
| } | |||||
| if (leaves.isNotEmpty()) { | if (leaves.isNotEmpty()) { | ||||
| leaves.forEach { leave -> | leaves.forEach { leave -> | ||||
| for (i in 0 until rowSize) { | |||||
| tempCell = sheet.getRow(8 + i).createCell(columnIndex) | |||||
| tempCell.setCellValue(0.0) | |||||
| tempCell.cellStyle.dataFormat = accountingStyle | |||||
| } | |||||
| dayInt = leave["recordDate"].toString().toInt() | dayInt = leave["recordDate"].toString().toInt() | ||||
| tempCell = sheet.getRow(dayInt.plus(7)).createCell(columnIndex) | tempCell = sheet.getRow(dayInt.plus(7)).createCell(columnIndex) | ||||
| tempCell.setCellValue(leave["leaveHours"] as Double) | tempCell.setCellValue(leave["leaveHours"] as Double) | ||||
| } | } | ||||
| } | } | ||||
| ///////////////////////////////////////////////////////// Leave Hours //////////////////////////////////////////////////////////////////// | |||||
| ///////////////////////////////////////////////////////// Leave Hours title //////////////////////////////////////////////////////////////////// | |||||
| tempCell = sheet.getRow(rowIndex).createCell(columnIndex) | tempCell = sheet.getRow(rowIndex).createCell(columnIndex) | ||||
| tempCell.setCellValue("Leave Hours") | tempCell.setCellValue("Leave Hours") | ||||
| tempCell.cellStyle = boldStyle | tempCell.cellStyle = boldStyle | ||||
| @@ -1027,6 +1061,73 @@ open class ReportService( | |||||
| return workbook | return workbook | ||||
| } | } | ||||
| private fun createProjectResourceOverconsumptionReport( | |||||
| team: String, | |||||
| customer: String, | |||||
| result: List<Map<String, Any>>, | |||||
| lowerLimit: Double, | |||||
| templatePath: String | |||||
| ):Workbook { | |||||
| val resource = ClassPathResource(templatePath) | |||||
| val templateInputStream = resource.inputStream | |||||
| val workbook: Workbook = XSSFWorkbook(templateInputStream) | |||||
| val accountingStyle = workbook.createDataFormat().getFormat("_(* #,##0.00_);_(* (#,##0.00);_(* \"-\"??_);_(@_)") | |||||
| val sheet: Sheet = workbook.getSheetAt(0) | |||||
| val alignCenterStyle = workbook.createCellStyle() | |||||
| alignCenterStyle.alignment = HorizontalAlignment.CENTER // Set the alignment to center | |||||
| var rowIndex = 1 // Assuming the location is in (1,2), which is the report date field | |||||
| var columnIndex = 2 | |||||
| var tempRow = sheet.getRow(rowIndex) | |||||
| var tempCell = tempRow.getCell(columnIndex) | |||||
| tempCell.setCellValue(FORMATTED_TODAY) | |||||
| rowIndex = 2 | |||||
| tempCell = sheet.getRow(rowIndex).getCell(columnIndex) | |||||
| tempCell.setCellValue(team) | |||||
| rowIndex = 3 | |||||
| tempCell = sheet.getRow(rowIndex).getCell(columnIndex) | |||||
| tempCell.setCellValue(customer) | |||||
| rowIndex = 6 | |||||
| columnIndex = 0 | |||||
| result.forEachIndexed { index, obj -> | |||||
| tempCell = sheet.getRow(rowIndex).createCell(columnIndex) | |||||
| tempCell.setCellValue((index + 1).toDouble()) | |||||
| val keys = obj.keys.toList() | |||||
| keys.forEachIndexed { keyIndex, key -> | |||||
| tempCell = sheet.getRow(rowIndex).getCell(columnIndex + keyIndex + 1) ?: sheet.getRow(rowIndex).createCell(columnIndex + keyIndex + 1) | |||||
| when (obj[key]) { | |||||
| is Double -> tempCell.setCellValue(obj[key] as Double) | |||||
| else -> tempCell.setCellValue(obj[key] as String ) | |||||
| } | |||||
| } | |||||
| rowIndex++ | |||||
| } | |||||
| val sheetCF = sheet.sheetConditionalFormatting | |||||
| val rule1 = sheetCF.createConditionalFormattingRule("AND(J7 >= $lowerLimit, J7 <= 1)") | |||||
| val rule2 = sheetCF.createConditionalFormattingRule("J7 > 1") | |||||
| var fillOrange = rule1.createPatternFormatting() | |||||
| fillOrange.setFillBackgroundColor(IndexedColors.LIGHT_ORANGE.index); | |||||
| fillOrange.setFillPattern(PatternFormatting.SOLID_FOREGROUND) | |||||
| var fillRed = rule2.createPatternFormatting() | |||||
| fillRed.setFillBackgroundColor(IndexedColors.ROSE.index); | |||||
| fillRed.setFillPattern(PatternFormatting.SOLID_FOREGROUND) | |||||
| val cfRules = arrayOf(rule1, rule2) | |||||
| val regions = arrayOf(CellRangeAddress.valueOf("\$J7:\$K${rowIndex+1}")) | |||||
| sheetCF.addConditionalFormatting(regions, cfRules); | |||||
| return workbook | |||||
| } | |||||
| //createLateStartReport | //createLateStartReport | ||||
| private fun createLateStartReport( | private fun createLateStartReport( | ||||
| team: Team, | team: Team, | ||||
| @@ -1173,6 +1274,81 @@ open class ReportService( | |||||
| return jdbcDao.queryForList(sql.toString(), args) | return jdbcDao.queryForList(sql.toString(), args) | ||||
| } | } | ||||
| open fun getProjectResourceOverconsumptionReport(args: Map<String, Any>): List<Map<String, Any>> { | |||||
| val sql = StringBuilder("WITH teamNormalConsumed AS (" | |||||
| + " SELECT " | |||||
| + " s.teamId, " | |||||
| + " pt.project_id, " | |||||
| + " SUM(tns.totalConsumed) AS totalConsumed " | |||||
| + " FROM ( " | |||||
| + " SELECT " | |||||
| + " t.staffId, " | |||||
| + " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) as totalConsumed, " | |||||
| + " t.projectTaskId AS taskId " | |||||
| + " FROM timesheet t " | |||||
| + " LEFT JOIN staff s ON t.staffId = s.id " | |||||
| + " GROUP BY t.staffId, t.projectTaskId " | |||||
| + " ) AS tns " | |||||
| + " INNER JOIN project_task pt ON tns.taskId = pt.id " | |||||
| + " JOIN staff s ON tns.staffId = s.id " | |||||
| + " JOIN team t ON s.teamId = t.id " | |||||
| + " GROUP BY teamId, project_id " | |||||
| + " ) " | |||||
| + " SELECT " | |||||
| + " p.code, " | |||||
| + " p.name, " | |||||
| + " t.code as team, " | |||||
| + " c.code as client, " | |||||
| + " p.expectedTotalFee as plannedBudget, " | |||||
| + " COALESCE((tns.totalConsumed * sa.hourlyRate), 0) as actualConsumedBudget, " | |||||
| + " COALESCE(p.totalManhour, 0) as plannedManhour, " | |||||
| + " COALESCE(tns.totalConsumed, 0) as actualConsumedManhour, " | |||||
| + " (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) as budgetConsumptionRate, " | |||||
| + " (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) as manhourConsumptionRate, " | |||||
| + " CASE " | |||||
| + " when (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= :lowerLimit and (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) <= 1 " | |||||
| + " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= :lowerLimit and (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) <= 1 " | |||||
| + " then 'Potential Overconsumption' " | |||||
| + " when (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= 1 " | |||||
| + " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= 1 " | |||||
| + " then 'Overconsumption' " | |||||
| + " else 'Within Budget' " | |||||
| + " END as status " | |||||
| + " FROM project p " | |||||
| + " LEFT JOIN team t ON p.teamLead = t.teamLead " | |||||
| + " LEFT JOIN staff s ON p.teamLead = s.id " | |||||
| + " LEFT JOIN salary sa ON s.salaryId = sa.salaryPoint " | |||||
| + " LEFT JOIN customer c ON p.customerId = c.id " | |||||
| + " left join teamNormalConsumed tns on tns.project_id = p.id " | |||||
| + " WHERE p.deleted = false " | |||||
| + " and p.status = 'On-going' " | |||||
| ) | |||||
| if (args != null) { | |||||
| var statusFilter: String = "" | |||||
| if (args.containsKey("teamId")) | |||||
| sql.append("and t.id = :teamId") | |||||
| if (args.containsKey("custId")) | |||||
| sql.append("and c.id = :custId") | |||||
| if (args.containsKey("status")) | |||||
| statusFilter = when (args.get("status")) { | |||||
| "Potential Overconsumption" -> "and " + | |||||
| " ((COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= 0.9 " + | |||||
| " and (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) <= 1 " + | |||||
| " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= 0.9 " + | |||||
| " and (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) <= 1)" | |||||
| "Overconsumption" -> "and " + | |||||
| " ((COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= 1 " + | |||||
| " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= 1) " | |||||
| "Within Budget" -> "and " + | |||||
| " ((COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) < 0.9 " + | |||||
| " and " + | |||||
| " (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) <= 0.9 " | |||||
| else -> "" | |||||
| } | |||||
| sql.append(statusFilter) | |||||
| } | |||||
| return jdbcDao.queryForList(sql.toString(), args) | |||||
| } | |||||
| open fun getManhoursSpent(projectId: Long, startMonth: String, endMonth: String): List<Map<String, Any>>{ | open fun getManhoursSpent(projectId: Long, startMonth: String, endMonth: String): List<Map<String, Any>>{ | ||||
| val sql = StringBuilder( | val sql = StringBuilder( | ||||
| " with cte_timesheet as (" | " with cte_timesheet as (" | ||||
| @@ -1,11 +1,23 @@ | |||||
| package com.ffii.tsms.modules.report.web | package com.ffii.tsms.modules.report.web | ||||
| import com.fasterxml.jackson.databind.ObjectMapper | |||||
| import com.fasterxml.jackson.module.kotlin.KotlinModule | |||||
| import com.ffii.tsms.modules.data.entity.Customer | |||||
| import com.ffii.tsms.modules.data.entity.StaffRepository | import com.ffii.tsms.modules.data.entity.StaffRepository | ||||
| //import com.ffii.tsms.modules.data.entity.projections.FinancialStatusReportInfo | //import com.ffii.tsms.modules.data.entity.projections.FinancialStatusReportInfo | ||||
| import com.ffii.tsms.modules.data.entity.projections.StaffSearchInfo | import com.ffii.tsms.modules.data.entity.projections.StaffSearchInfo | ||||
| import com.ffii.tsms.modules.data.entity.Team | |||||
| import com.ffii.tsms.modules.data.service.CustomerService | |||||
| import com.ffii.tsms.modules.data.service.TeamService | |||||
| import com.ffii.tsms.modules.project.entity.* | import com.ffii.tsms.modules.project.entity.* | ||||
| import com.ffii.tsms.modules.report.service.ReportService | |||||
| import com.ffii.tsms.modules.project.entity.projections.ProjectResourceReport | |||||
| import com.ffii.tsms.modules.project.service.InvoiceService | import com.ffii.tsms.modules.project.service.InvoiceService | ||||
| import com.ffii.tsms.modules.report.service.ReportService | |||||
| import com.ffii.tsms.modules.report.web.model.FinancialStatusReportRequest | |||||
| import com.ffii.tsms.modules.report.web.model.ProjectCashFlowReportRequest | |||||
| import com.ffii.tsms.modules.report.web.model.ProjectResourceOverconsumptionReport | |||||
| import com.ffii.tsms.modules.report.web.model.StaffMonthlyWorkHourAnalysisReportRequest | |||||
| import com.ffii.tsms.modules.report.web.model.LateStartReportRequest | |||||
| import com.ffii.tsms.modules.project.entity.ProjectRepository | import com.ffii.tsms.modules.project.entity.ProjectRepository | ||||
| import com.ffii.tsms.modules.timesheet.entity.LeaveRepository | import com.ffii.tsms.modules.timesheet.entity.LeaveRepository | ||||
| import com.ffii.tsms.modules.timesheet.entity.TimesheetRepository | import com.ffii.tsms.modules.timesheet.entity.TimesheetRepository | ||||
| @@ -13,17 +25,11 @@ import com.ffii.tsms.modules.timesheet.entity.projections.ProjectMonthlyHoursWit | |||||
| import jakarta.validation.Valid | import jakarta.validation.Valid | ||||
| import org.springframework.core.io.ByteArrayResource | import org.springframework.core.io.ByteArrayResource | ||||
| import org.springframework.core.io.Resource | import org.springframework.core.io.Resource | ||||
| import org.springframework.http.ResponseEntity | |||||
| import org.springframework.web.bind.ServletRequestBindingException | |||||
| import org.springframework.web.bind.annotation.GetMapping | |||||
| import org.springframework.web.bind.annotation.PathVariable | |||||
| import org.springframework.web.bind.annotation.PostMapping | |||||
| import org.springframework.web.bind.annotation.RequestBody | |||||
| import org.springframework.web.bind.annotation.RequestMapping | |||||
| import org.springframework.web.bind.annotation.RequestParam | |||||
| import org.springframework.web.bind.annotation.RestController | |||||
| import org.springframework.http.HttpHeaders | import org.springframework.http.HttpHeaders | ||||
| import org.springframework.http.MediaType | import org.springframework.http.MediaType | ||||
| import org.springframework.http.ResponseEntity | |||||
| import org.springframework.web.bind.ServletRequestBindingException | |||||
| import org.springframework.web.bind.annotation.* | |||||
| import java.io.IOException | import java.io.IOException | ||||
| import java.time.LocalDate | import java.time.LocalDate | ||||
| import java.net.URLEncoder | import java.net.URLEncoder | ||||
| @@ -32,7 +38,6 @@ import org.springframework.stereotype.Controller | |||||
| import com.ffii.tsms.modules.data.entity.TeamRepository | import com.ffii.tsms.modules.data.entity.TeamRepository | ||||
| import com.ffii.tsms.modules.data.entity.CustomerRepository | import com.ffii.tsms.modules.data.entity.CustomerRepository | ||||
| import org.springframework.data.jpa.repository.JpaRepository | import org.springframework.data.jpa.repository.JpaRepository | ||||
| import com.ffii.tsms.modules.data.entity.Customer | |||||
| import com.ffii.tsms.modules.project.entity.Project | import com.ffii.tsms.modules.project.entity.Project | ||||
| import com.ffii.tsms.modules.report.web.model.* | import com.ffii.tsms.modules.report.web.model.* | ||||
| @@ -49,6 +54,8 @@ class ReportController( | |||||
| private val customerRepository: CustomerRepository, | private val customerRepository: CustomerRepository, | ||||
| private val staffRepository: StaffRepository, | private val staffRepository: StaffRepository, | ||||
| private val leaveRepository: LeaveRepository, | private val leaveRepository: LeaveRepository, | ||||
| private val teamService: TeamService, | |||||
| private val customerService: CustomerService, | |||||
| private val invoiceService: InvoiceService) { | private val invoiceService: InvoiceService) { | ||||
| @PostMapping("/fetchProjectsFinancialStatusReport") | @PostMapping("/fetchProjectsFinancialStatusReport") | ||||
| @@ -87,13 +94,16 @@ class ReportController( | |||||
| val nextMonth = request.yearMonth.plusMonths(1).atDay(1) | val nextMonth = request.yearMonth.plusMonths(1).atDay(1) | ||||
| val staff = staffRepository.findById(request.id).orElseThrow() | val staff = staffRepository.findById(request.id).orElseThrow() | ||||
| println(thisMonth) | |||||
| println(nextMonth) | |||||
| val args: Map<String, Any> = mutableMapOf( | val args: Map<String, Any> = mutableMapOf( | ||||
| "staffId" to request.id, | "staffId" to request.id, | ||||
| "startDate" to thisMonth, | "startDate" to thisMonth, | ||||
| "endDate" to nextMonth, | "endDate" to nextMonth, | ||||
| ) | ) | ||||
| val timesheets= excelReportService.getTimesheet(args) | |||||
| val leaves= excelReportService.getLeaves(args) | |||||
| val timesheets = excelReportService.getTimesheet(args) | |||||
| val leaves = excelReportService.getLeaves(args) | |||||
| println(leaves) | |||||
| val reportResult: ByteArray = excelReportService.generateStaffMonthlyWorkHourAnalysisReport(thisMonth, staff, timesheets, leaves) | val reportResult: ByteArray = excelReportService.generateStaffMonthlyWorkHourAnalysisReport(thisMonth, staff, timesheets, leaves) | ||||
| // val mediaType: MediaType = MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") | // val mediaType: MediaType = MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") | ||||
| @@ -102,6 +112,34 @@ class ReportController( | |||||
| .header("filename", "Monthly Work Hours Analysis Report - " + staff.name + " - " + LocalDate.now() + ".xlsx") | .header("filename", "Monthly Work Hours Analysis Report - " + staff.name + " - " + LocalDate.now() + ".xlsx") | ||||
| .body(ByteArrayResource(reportResult)) | .body(ByteArrayResource(reportResult)) | ||||
| } | } | ||||
| private val mapper = ObjectMapper().registerModule(KotlinModule()) | |||||
| @PostMapping("/ProjectResourceOverconsumptionReport") | |||||
| @Throws(ServletRequestBindingException::class, IOException::class) | |||||
| fun ProjectResourceOverconsumptionReport(@RequestBody @Valid request: ProjectResourceOverconsumptionReport): ResponseEntity<Resource> { | |||||
| val lowerLimit = request.lowerLimit | |||||
| var team: String = "All" | |||||
| var customer: String = "All" | |||||
| val args: MutableMap<String, Any> = mutableMapOf( | |||||
| "status" to request.status, | |||||
| "lowerLimit" to lowerLimit | |||||
| ) | |||||
| if (request.teamId != null) { | |||||
| args["teamId"] = request.teamId | |||||
| team = teamService.find(request.teamId).orElseThrow().name | |||||
| } | |||||
| if (request.custId != null) { | |||||
| args["custId"] = request.custId | |||||
| customer = customerService.find(request.custId).orElseThrow().name | |||||
| } | |||||
| val result: List<Map<String, Any>> = excelReportService.getProjectResourceOverconsumptionReport(args); | |||||
| val reportResult: ByteArray = excelReportService.generateProjectResourceOverconsumptionReport(team, customer, result, lowerLimit) | |||||
| // val mediaType: MediaType = MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") | |||||
| return ResponseEntity.ok() | |||||
| // .contentType(mediaType) | |||||
| .header("filename", "Project Resource Overconsumption Report - " + " - " + LocalDate.now() + ".xlsx") | |||||
| .body(ByteArrayResource(reportResult)) | |||||
| } | |||||
| @GetMapping("/test/{id}") | @GetMapping("/test/{id}") | ||||
| fun test(@PathVariable id: Long): List<Invoice> { | fun test(@PathVariable id: Long): List<Invoice> { | ||||
| @@ -30,4 +30,10 @@ data class LateStartReportRequest ( | |||||
| val remainedDateTo: LocalDate, | val remainedDateTo: LocalDate, | ||||
| val customer: String, | val customer: String, | ||||
| val reportDate: LocalDate | val reportDate: LocalDate | ||||
| ) | |||||
| data class ProjectResourceOverconsumptionReport ( | |||||
| val teamId: Long?, | |||||
| val custId: Long?, | |||||
| val status: String, | |||||
| val lowerLimit: Double | |||||
| ) | ) | ||||
| @@ -0,0 +1,20 @@ | |||||
| -- liquibase formatted sql | |||||
| -- changeset cyril:project | |||||
| ALTER TABLE `project` | |||||
| ADD COLUMN `customerContactId` INT NULL DEFAULT NULL AFTER `mainProjectId`, | |||||
| ADD COLUMN `subsidiaryContactId` INT NULL DEFAULT NULL AFTER `customerContactId`, | |||||
| ADD INDEX `FK_PROJECT_ON_CUSTOMERCONTACTID` (`customerContactId` ASC) VISIBLE, | |||||
| ADD INDEX `FK_PROJECT_ON_SUBSIDIARYCONTACTID` (`subsidiaryContactId` ASC) VISIBLE; | |||||
| ; | |||||
| ALTER TABLE `project` | |||||
| ADD CONSTRAINT `FK_PROJECT_ON_CUSTOMERCONTACTID` | |||||
| FOREIGN KEY (`customerContactId`) | |||||
| REFERENCES `customer_contact` (`id`) | |||||
| ON DELETE NO ACTION | |||||
| ON UPDATE NO ACTION, | |||||
| ADD CONSTRAINT `FK_PROJECT_ON_SUBSIDIARYCONTACTID` | |||||
| FOREIGN KEY (`subsidiaryContactId`) | |||||
| REFERENCES `subsidiary_contact` (`id`) | |||||
| ON DELETE NO ACTION | |||||
| ON UPDATE NO ACTION; | |||||