# 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; |