Ver código fonte

Merge branch 'master' of https://git.2fi-solutions.com/davidhui/TSMS-backend

# Conflicts:
#	src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt
#	src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt
tags/Baseline_30082024_BACKEND_UAT
MSI\2Fi 1 ano atrás
pai
commit
f953a7a6f1
16 arquivos alterados com 457 adições e 104 exclusões
  1. +1
    -1
      src/main/java/com/ffii/tsms/modules/data/service/CustomerContactService.kt
  2. +1
    -1
      src/main/java/com/ffii/tsms/modules/data/service/StaffsService.kt
  3. +8
    -0
      src/main/java/com/ffii/tsms/modules/project/entity/Project.kt
  4. +3
    -3
      src/main/java/com/ffii/tsms/modules/project/entity/ProjectRepository.kt
  5. +15
    -0
      src/main/java/com/ffii/tsms/modules/project/entity/projections/ProjectResourceReport.kt
  6. +3
    -0
      src/main/java/com/ffii/tsms/modules/project/entity/projections/ProjectSearchInfo.kt
  7. +113
    -65
      src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt
  8. +8
    -1
      src/main/java/com/ffii/tsms/modules/project/web/ProjectsController.kt
  9. +1
    -0
      src/main/java/com/ffii/tsms/modules/project/web/models/EditProjectDetails.kt
  10. +30
    -0
      src/main/java/com/ffii/tsms/modules/project/web/models/MainProjectDetails.kt
  11. +1
    -0
      src/main/java/com/ffii/tsms/modules/project/web/models/NewProjectRequest.kt
  12. +196
    -20
      src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt
  13. +51
    -13
      src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt
  14. +6
    -0
      src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt
  15. +20
    -0
      src/main/resources/db/changelog/changes/20240522_01_cyril/01_update_project.sql
  16. BIN
      src/main/resources/templates/report/AR03_Resource Overconsumption.xlsx

+ 1
- 1
src/main/java/com/ffii/tsms/modules/data/service/CustomerContactService.kt Ver arquivo

@@ -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>) {


+ 1
- 1
src/main/java/com/ffii/tsms/modules/data/service/StaffsService.kt Ver arquivo

@@ -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 {


+ 8
- 0
src/main/java/com/ffii/tsms/modules/project/entity/Project.kt Ver arquivo

@@ -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
}

+ 3
- 3
src/main/java/com/ffii/tsms/modules/project/entity/ProjectRepository.kt Ver arquivo

@@ -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>
}

+ 15
- 0
src/main/java/com/ffii/tsms/modules/project/entity/projections/ProjectResourceReport.kt Ver arquivo

@@ -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
)

+ 3
- 0
src/main/java/com/ffii/tsms/modules/project/entity/projections/ProjectSearchInfo.kt Ver arquivo

@@ -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?



+ 113
- 65
src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt Ver arquivo

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

+ 8
- 1
src/main/java/com/ffii/tsms/modules/project/web/ProjectsController.kt Ver arquivo

@@ -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) {


+ 1
- 0
src/main/java/com/ffii/tsms/modules/project/web/models/EditProjectDetails.kt Ver arquivo

@@ -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?,


+ 30
- 0
src/main/java/com/ffii/tsms/modules/project/web/models/MainProjectDetails.kt Ver arquivo

@@ -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?,
)

+ 1
- 0
src/main/java/com/ffii/tsms/modules/project/web/models/NewProjectRequest.kt Ver arquivo

@@ -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,


+ 196
- 20
src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt Ver arquivo

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


+ 51
- 13
src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt Ver arquivo

@@ -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> {


+ 6
- 0
src/main/java/com/ffii/tsms/modules/report/web/model/ReportRequest.kt Ver arquivo

@@ -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
)

+ 20
- 0
src/main/resources/db/changelog/changes/20240522_01_cyril/01_update_project.sql Ver arquivo

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

BIN
src/main/resources/templates/report/AR03_Resource Overconsumption.xlsx Ver arquivo


Carregando…
Cancelar
Salvar