# 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 { | |||
return customerContactRepository.findById(contactId).orElseThrow() | |||
return customerContactRepository.findById(contactId).orElse(null) | |||
} | |||
fun saveContactsByCustomer(saveCustomerId: Long, saveCustomerContact: List<CustomerContact>) { | |||
@@ -178,7 +178,7 @@ open class StaffsService( | |||
val company = companyRepository.findById(req.companyId).orElseThrow() | |||
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 salary = salaryRepository.findById(req.salaryId).orElseThrow() | |||
val salary = salaryRepository.findBySalaryPoint(req.salaryId).orElseThrow() | |||
val department = departmentRepository.findById(req.departmentId).orElseThrow() | |||
staff.apply { | |||
@@ -123,4 +123,12 @@ open class Project : BaseEntity<Long>() { | |||
// @JsonBackReference | |||
@JoinColumn(name = "mainProjectId") | |||
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 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" + | |||
"") | |||
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 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 status: String? | |||
@get:Value("#{target.mainProject?.code}") | |||
val mainProject: String? | |||
@get:Value("#{target.projectCategory.name}") | |||
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.data.service.CustomerService | |||
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.Milestone | |||
import com.ffii.tsms.modules.project.entity.projections.InvoiceInfoSearchInfo | |||
@@ -44,10 +45,12 @@ open class ProjectsService( | |||
private val taskGroupRepository: TaskGroupRepository, | |||
private val timesheetRepository: TimesheetRepository, | |||
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> { | |||
return projectRepository.findProjectSearchInfoByOrderByCreatedDesc().sortedByDescending { it.status?.lowercase() != "deleted" } | |||
return projectRepository.findProjectSearchInfoByOrderByCreatedDesc() | |||
.sortedByDescending { it.status?.lowercase() != "deleted" } | |||
} | |||
open fun allInvoices(): List<InvoiceSearchInfo> { | |||
@@ -63,63 +66,57 @@ open class ProjectsService( | |||
} | |||
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> { | |||
return SecurityUtils.getUser().getOrNull()?.let { user -> | |||
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() | |||
} | |||
open fun allProjectWithTasks(): List<ProjectWithTasks> { | |||
return projectRepository.findAll().map { project -> | |||
ProjectWithTasks( | |||
id = project.id!!, | |||
ProjectWithTasks(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) -> | |||
.associateBy { milestone -> milestone.taskGroup!!.id!! }.mapValues { (_, milestone) -> | |||
MilestoneInfo( | |||
startDate = milestone.startDate?.format(DateTimeFormatter.ISO_LOCAL_DATE), | |||
endDate = milestone.endDate?.format(DateTimeFormatter.ISO_LOCAL_DATE) | |||
) | |||
} | |||
) | |||
}) | |||
} | |||
} | |||
@@ -135,15 +132,28 @@ open class ProjectsService( | |||
if (latestProjectCode != null) { | |||
val lastFix = latestProjectCode | |||
return "$prefix-" + String.format("%04d", lastFix + 1L) | |||
return "$prefix-" + String.format("%04d", lastFix + 1L) | |||
} else { | |||
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 | |||
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 serviceType = serviceTypeRepository.findById(request.serviceTypeId).orElseThrow() | |||
val contractType = contractTypeRepository.findById(request.contractTypeId).orElseThrow() | |||
@@ -158,6 +168,7 @@ open class ProjectsService( | |||
val subsidiaryContact = subsidiaryContactRepository.findById(request.clientContactId).orElse(null) | |||
val clientContact = customerContactService.findByContactId(request.clientContactId) | |||
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 taskGroupMap = tasksService.allTaskGroups().associateBy { it.id } | |||
@@ -169,7 +180,7 @@ open class ProjectsService( | |||
project.apply { | |||
name = request.projectName | |||
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 | |||
totalManhour = request.totalManhour | |||
actualStart = request.projectActualStart | |||
@@ -179,6 +190,7 @@ open class ProjectsService( | |||
else if (this.actualStart != null) "On-going" | |||
else "Pending To Start" | |||
isClpProject = request.isClpProject | |||
this.mainProject = mainProject | |||
this.projectCategory = projectCategory | |||
this.fundingType = fundingType | |||
@@ -191,10 +203,15 @@ open class ProjectsService( | |||
this.teamLead = teamLead | |||
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.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 taskGroup = taskGroupRepository.findById(taskStageId).orElse(TaskGroup()) ?: TaskGroup() | |||
val milestone = if (project.id != null && project.id!! > 0L) milestoneRepository.findByProjectAndTaskGroup( | |||
project, | |||
taskGroup | |||
project, taskGroup | |||
) ?: Milestone() else Milestone() | |||
milestone.apply { | |||
val newMilestone = this | |||
@@ -233,8 +249,7 @@ open class ProjectsService( | |||
taskGroupAllocation.taskIds.map { taskId -> | |||
val projectTask = | |||
if (project.id != null && project.id!! > 0L) projectTaskRepository.findByProjectAndTask( | |||
project, | |||
allTasksMap[taskId]!! | |||
project, allTasksMap[taskId]!! | |||
) ?: ProjectTask() else ProjectTask() | |||
projectTask.apply { | |||
@@ -252,8 +267,7 @@ open class ProjectsService( | |||
val gradeAllocations = request.manhourPercentageByGrade.entries.map { | |||
val gradeAllocation = | |||
if (project.id != null && project.id!! > 0L) gradeAllocationRepository.findByProjectAndGrade( | |||
project, | |||
gradeMap[it.key]!! | |||
project, gradeMap[it.key]!! | |||
) ?: GradeAllocation() else GradeAllocation() | |||
gradeAllocation.apply { | |||
@@ -313,16 +327,16 @@ open class ProjectsService( | |||
val project = projectRepository.findById(projectId) | |||
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!! } | |||
EditProjectDetails( | |||
projectId = it.id, | |||
EditProjectDetails(projectId = it.id, | |||
projectDeleted = it.deleted, | |||
projectCode = it.code, | |||
projectName = it.name, | |||
@@ -333,6 +347,7 @@ open class ProjectsService( | |||
projectActualEnd = it.actualEnd, | |||
projectStatus = it.status, | |||
isClpProject = it.isClpProject, | |||
mainProjectId = it.mainProject?.id, | |||
serviceTypeId = it.serviceType?.id, | |||
fundingTypeId = it.fundingType?.id, | |||
contractTypeId = it.contractType?.id, | |||
@@ -341,7 +356,8 @@ open class ProjectsService( | |||
buildingTypeIds = it.buildingTypes.mapNotNull { buildingType -> buildingType.id }, | |||
workNatureIds = it.workNatures.mapNotNull { workNature -> workNature.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, | |||
totalManhour = it.totalManhour, | |||
manhourPercentageByGrade = gradeAllocationRepository.findByProject(it) | |||
@@ -349,8 +365,7 @@ open class ProjectsService( | |||
.associate { allocation -> Pair(allocation.grade!!.id!!, allocation.manhour ?: 0.0) }, | |||
taskGroups = projectTaskRepository.findAllByProject(it) | |||
.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( | |||
taskIds = tasks.mapNotNull { task -> task.id }, | |||
percentAllocation = milestoneMap[taskGroupId]?.stagePercentAllocation ?: 0.0 | |||
@@ -359,8 +374,9 @@ open class ProjectsService( | |||
allocatedStaffIds = staffAllocationRepository.findByProject(it) | |||
.mapNotNull { allocation -> allocation.staff?.id }, | |||
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), | |||
payments = milestone.milestonePayments.map { payment -> | |||
PaymentInputs( | |||
@@ -369,8 +385,7 @@ open class ProjectsService( | |||
description = payment.description!!, | |||
date = payment.date!!.format(DateTimeFormatter.ISO_LOCAL_DATE) | |||
) | |||
} | |||
) | |||
}) | |||
}, | |||
expectedProjectFee = it.expectedTotalFee | |||
) | |||
@@ -400,4 +415,37 @@ open class ProjectsService( | |||
open fun allWorkNatures(): List<WorkNature> { | |||
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.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.ProjectCategory | |||
import com.ffii.tsms.modules.project.entity.ProjectRepository | |||
import com.ffii.tsms.modules.project.service.ProjectsService | |||
import com.ffii.tsms.modules.project.web.models.* | |||
import jakarta.validation.Valid | |||
@@ -12,12 +14,17 @@ import org.springframework.web.bind.annotation.* | |||
@RestController | |||
@RequestMapping("/projects") | |||
class ProjectsController(private val projectsService: ProjectsService) { | |||
class ProjectsController(private val projectsService: ProjectsService, private val projectRepository: ProjectRepository) { | |||
@GetMapping | |||
fun allProjects(): List<ProjectSearchInfo> { | |||
return projectsService.allProjects() | |||
} | |||
@GetMapping("/main") | |||
fun allMainProjects(): List<MainProjectDetails> { | |||
return projectsService.allMainProjects() | |||
} | |||
@DeleteMapping("/{id}") | |||
@ResponseStatus(HttpStatus.NO_CONTENT) | |||
fun deleteProject(@PathVariable id: Long) { | |||
@@ -15,6 +15,7 @@ data class EditProjectDetails( | |||
val projectActualEnd: LocalDate?, | |||
val projectStatus: String?, | |||
val isClpProject: Boolean?, | |||
val mainProjectId: Long?, | |||
val serviceTypeId: 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 projectActualEnd: LocalDate?, | |||
val isClpProject: Boolean?, | |||
val mainProjectId: Long?, | |||
val serviceTypeId: 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.project.entity.Invoice | |||
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.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.LogFactory | |||
import org.apache.poi.ss.usermodel.* | |||
@@ -20,6 +25,7 @@ import org.springframework.stereotype.Service | |||
import java.io.ByteArrayOutputStream | |||
import java.io.IOException | |||
import java.math.BigDecimal | |||
import java.sql.Time | |||
import java.time.LocalDate | |||
import java.time.YearMonth | |||
import java.time.format.DateTimeFormatter | |||
@@ -27,6 +33,8 @@ import java.util.* | |||
data class DayInfo(val date: String?, val weekday: String?) | |||
@Service | |||
open class ReportService( | |||
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 SALART_LIST_TEMPLATE = "templates/report/Salary Template.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 ||============================== // | |||
@@ -143,6 +152,29 @@ open class ReportService( | |||
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) | |||
fun exportSalaryList(salarys: List<Salary>): ByteArray { | |||
// Generate the Excel report with query results | |||
@@ -733,10 +765,13 @@ open class ReportService( | |||
var projectList: List<String> = listOf() | |||
println("----timesheets-----") | |||
println(timesheets) | |||
println("----leaves-----") | |||
println(leaves) | |||
// result = timesheet record mapped | |||
var result: Map<String, Any> = mapOf() | |||
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( | |||
{ it["id"].toString() }, | |||
{ mapOf( | |||
@@ -916,41 +951,40 @@ open class ReportService( | |||
columnSize++ | |||
} | |||
println(columnSize) | |||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// | |||
/////////////////////////////////////////////////////////////// main data ////////////////////////////////////////////////////////////// | |||
if (timesheets.isNotEmpty()) { | |||
projectList.forEach { _ -> | |||
projectList.forEachIndexed { index, _ -> | |||
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.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++ | |||
} | |||
} | |||
// 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()) { | |||
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() | |||
tempCell = sheet.getRow(dayInt.plus(7)).createCell(columnIndex) | |||
tempCell.setCellValue(leave["leaveHours"] as Double) | |||
} | |||
} | |||
///////////////////////////////////////////////////////// Leave Hours //////////////////////////////////////////////////////////////////// | |||
///////////////////////////////////////////////////////// Leave Hours title //////////////////////////////////////////////////////////////////// | |||
tempCell = sheet.getRow(rowIndex).createCell(columnIndex) | |||
tempCell.setCellValue("Leave Hours") | |||
tempCell.cellStyle = boldStyle | |||
@@ -1027,6 +1061,73 @@ open class ReportService( | |||
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 | |||
private fun createLateStartReport( | |||
team: Team, | |||
@@ -1173,6 +1274,81 @@ open class ReportService( | |||
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>>{ | |||
val sql = StringBuilder( | |||
" with cte_timesheet as (" | |||
@@ -1,11 +1,23 @@ | |||
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.projections.FinancialStatusReportInfo | |||
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.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.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.timesheet.entity.LeaveRepository | |||
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 org.springframework.core.io.ByteArrayResource | |||
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.MediaType | |||
import org.springframework.http.ResponseEntity | |||
import org.springframework.web.bind.ServletRequestBindingException | |||
import org.springframework.web.bind.annotation.* | |||
import java.io.IOException | |||
import java.time.LocalDate | |||
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.CustomerRepository | |||
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.report.web.model.* | |||
@@ -49,6 +54,8 @@ class ReportController( | |||
private val customerRepository: CustomerRepository, | |||
private val staffRepository: StaffRepository, | |||
private val leaveRepository: LeaveRepository, | |||
private val teamService: TeamService, | |||
private val customerService: CustomerService, | |||
private val invoiceService: InvoiceService) { | |||
@PostMapping("/fetchProjectsFinancialStatusReport") | |||
@@ -87,13 +94,16 @@ class ReportController( | |||
val nextMonth = request.yearMonth.plusMonths(1).atDay(1) | |||
val staff = staffRepository.findById(request.id).orElseThrow() | |||
println(thisMonth) | |||
println(nextMonth) | |||
val args: Map<String, Any> = mutableMapOf( | |||
"staffId" to request.id, | |||
"startDate" to thisMonth, | |||
"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 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") | |||
.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}") | |||
fun test(@PathVariable id: Long): List<Invoice> { | |||
@@ -30,4 +30,10 @@ data class LateStartReportRequest ( | |||
val remainedDateTo: LocalDate, | |||
val customer: String, | |||
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; |