Parcourir la source

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

tags/Baseline_30082024_BACKEND_UAT
MSI\2Fi il y a 1 an
Parent
révision
080c4186f1
19 fichiers modifiés avec 329 ajouts et 83 suppressions
  1. +16
    -0
      src/main/java/com/ffii/tsms/modules/data/entity/Subsidiary.java
  2. +5
    -6
      src/main/java/com/ffii/tsms/modules/data/entity/SubsidiaryContact.java
  3. +2
    -0
      src/main/java/com/ffii/tsms/modules/data/entity/SubsidiaryRepository.java
  4. +3
    -0
      src/main/java/com/ffii/tsms/modules/data/entity/projections/StaffSearchInfo.kt
  5. +8
    -2
      src/main/java/com/ffii/tsms/modules/data/service/StaffsService.kt
  6. +1
    -1
      src/main/java/com/ffii/tsms/modules/data/service/TeamService.kt
  7. +6
    -0
      src/main/java/com/ffii/tsms/modules/data/web/SkillController.kt
  8. +15
    -10
      src/main/java/com/ffii/tsms/modules/data/web/models/NewStaffRequest.kt
  9. +10
    -0
      src/main/java/com/ffii/tsms/modules/project/entity/Project.kt
  10. +3
    -0
      src/main/java/com/ffii/tsms/modules/project/entity/ProjectRepository.kt
  11. +1
    -0
      src/main/java/com/ffii/tsms/modules/project/entity/projections/ProjectSearchInfo.kt
  12. +128
    -61
      src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt
  13. +3
    -0
      src/main/java/com/ffii/tsms/modules/project/web/models/EditProjectDetails.kt
  14. +6
    -2
      src/main/java/com/ffii/tsms/modules/project/web/models/NewProjectRequest.kt
  15. +83
    -1
      src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt
  16. +16
    -0
      src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt
  17. +13
    -0
      src/main/resources/db/changelog/changes/20240513_01_cyril/01_update_project.sql
  18. +5
    -0
      src/main/resources/db/changelog/changes/20240513_01_cyril/02_update_project.sql
  19. +5
    -0
      src/main/resources/db/changelog/changes/20240514_01_cyril/01_update_project.sql

+ 16
- 0
src/main/java/com/ffii/tsms/modules/data/entity/Subsidiary.java Voir le fichier

@@ -1,10 +1,14 @@
package com.ffii.tsms.modules.data.entity;

import com.fasterxml.jackson.annotation.JsonManagedReference;
import com.ffii.core.entity.BaseEntity;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;

import java.time.LocalTime;
import java.util.List;

import static jakarta.persistence.CascadeType.ALL;

@Entity
@Table(name = "subsidiary")
@@ -31,6 +35,10 @@ public class Subsidiary extends BaseEntity<Long> {
@JoinColumn(name = "typeId")
private SubsidiaryType subsidiaryType;

@OneToMany(mappedBy = "subsidiary", cascade = ALL, orphanRemoval = true)
@JsonManagedReference
private List<SubsidiaryContact> subsidiaryContacts;

public String getAddress() {
return address;
}
@@ -78,4 +86,12 @@ public class Subsidiary extends BaseEntity<Long> {
public void setSubsidiaryType(SubsidiaryType subsidiaryType) {
this.subsidiaryType = subsidiaryType;
}

public List<SubsidiaryContact> getSubsidiaryContacts() {
return subsidiaryContacts;
}

public void setSubsidiaryContacts(List<SubsidiaryContact> subsidiaryContacts) {
this.subsidiaryContacts = subsidiaryContacts;
}
}

+ 5
- 6
src/main/java/com/ffii/tsms/modules/data/entity/SubsidiaryContact.java Voir le fichier

@@ -1,18 +1,17 @@
package com.ffii.tsms.modules.data.entity;

import com.fasterxml.jackson.annotation.JsonBackReference;
import com.fasterxml.jackson.annotation.JsonManagedReference;
import com.ffii.core.entity.IdEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;

@Entity
@Table(name = "subsidiary_contact")
public class SubsidiaryContact extends IdEntity<Long> {
@NotNull
@OneToOne
@ManyToOne
@JsonBackReference
@JoinColumn(name = "subsidiaryId")
private Subsidiary subsidiary;



+ 2
- 0
src/main/java/com/ffii/tsms/modules/data/entity/SubsidiaryRepository.java Voir le fichier

@@ -9,5 +9,7 @@ import java.util.Optional;
public interface SubsidiaryRepository extends AbstractRepository<Subsidiary, Long> {
List<Subsidiary> findAllByDeletedFalse();

List<Subsidiary> findAllByDeletedFalseAndIdIn(List<Long> id);

Optional<Subsidiary> findByCode(@Param("code") String code);
}

+ 3
- 0
src/main/java/com/ffii/tsms/modules/data/entity/projections/StaffSearchInfo.kt Voir le fichier

@@ -22,4 +22,7 @@ interface StaffSearchInfo {
@get:Value("#{target.currentPosition?.name}")
val currentPosition: String?

@get:Value("#{target.user?.id}")
val userId: Long?

}

+ 8
- 2
src/main/java/com/ffii/tsms/modules/data/service/StaffsService.kt Voir le fichier

@@ -1,5 +1,6 @@
package com.ffii.tsms.modules.data.service

import com.ffii.core.exception.UnprocessableEntityException
import com.ffii.core.support.AbstractBaseEntityService
import com.ffii.core.support.JdbcDao
import com.ffii.tsms.modules.common.SecurityUtils
@@ -8,12 +9,10 @@ import com.ffii.tsms.modules.data.entity.projections.StaffSearchInfo
import com.ffii.tsms.modules.data.web.models.NewStaffRequest
import com.ffii.tsms.modules.user.entity.User
import com.ffii.tsms.modules.user.entity.UserRepository
import org.springframework.data.jpa.domain.AbstractPersistable_.id
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.util.*
import java.util.stream.Collectors
import kotlin.jvm.optionals.getOrNull


@@ -96,6 +95,13 @@ open class StaffsService(

@Transactional(rollbackFor = [Exception::class])
open fun saveStaff(req: NewStaffRequest): Staff {
// if (req.staffId)
val checkStaffIdList: List<StaffSearchInfo> = staffRepository.findStaffSearchInfoByAndDeletedFalse()
checkStaffIdList.forEach{ s ->
if (s.staffId == req.staffId) {
throw UnprocessableEntityException("Duplicated StaffId Found")
}
}
val currentPosition = positionRepository.findById(req.currentPositionId).orElseThrow()
val joinPosition = positionRepository.findById(req.joinPositionId).orElseThrow()
val company = companyRepository.findById(req.companyId).orElseThrow()


+ 1
- 1
src/main/java/com/ffii/tsms/modules/data/service/TeamService.kt Voir le fichier

@@ -39,7 +39,7 @@ open class TeamService(
@Transactional(rollbackFor = [Exception::class])
open fun saveTeam(req: NewTeamRequest): Team {
val ids = req.addStaffIds!!
println(ids)
// println(ids)
val teamLead = staffRepository.findById(ids[0]).orElseThrow()
val teamName = "Team " + teamLead.name



+ 6
- 0
src/main/java/com/ffii/tsms/modules/data/web/SkillController.kt Voir le fichier

@@ -7,6 +7,7 @@ import com.ffii.tsms.modules.data.service.SkillService
import com.ffii.tsms.modules.data.web.models.NewSkillRequest
import jakarta.servlet.http.HttpServletRequest
import jakarta.validation.Valid
import org.springframework.http.HttpStatus
import org.springframework.web.bind.ServletRequestBindingException
import org.springframework.web.bind.annotation.*

@@ -25,6 +26,11 @@ class SkillController(private val skillService: SkillService) {
args["id"] = id
return skillService.list(args);
}
@DeleteMapping("/delete/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
fun delete(@PathVariable id: Long?) {
skillService.markDelete(id)
}

@GetMapping
fun list(): List<Map<String, Any>> {


+ 15
- 10
src/main/java/com/ffii/tsms/modules/data/web/models/NewStaffRequest.kt Voir le fichier

@@ -5,10 +5,6 @@ import jakarta.validation.constraints.NotNull
import java.time.LocalDate

data class NewStaffRequest(
val id: Long?,

// @field:NotNull(message = "Staff userId cannot be empty")
// val userId: Long,
@field:NotBlank(message = "Staff name cannot be empty")
val name: String,
@field:NotBlank(message = "Staff staffId cannot be empty")
@@ -17,21 +13,30 @@ data class NewStaffRequest(
val companyId: Long,
@field:NotNull(message = "Staff salaryId cannot be empty")
val salaryId: Long,
// @field:NotNull(message = "Staff skillSetId cannot be empty")
val skillSetId: List<Long>?,
@field:NotNull(message = "joinDate cannot be empty")
val joinDate: LocalDate,
@field:NotNull(message = "Staff currentPositionId cannot be empty")
val currentPositionId: Long,
// val salaryEffId: Long,
@field:NotNull(message = "Staff joinPositionId cannot be empty")
val joinPositionId: Long,
val gradeId: Long?,
val teamId: Long?,
@field:NotNull(message = "Staff departmentId cannot be empty")
val departmentId: Long,
@field:NotBlank(message = "Staff phone1 cannot be empty")
val phone1: String,
val phone2: String?,
@field:NotBlank(message = "Staff email cannot be empty")
val email: String,
@field:NotBlank(message = "Staff emergContactName cannot be empty")
val emergContactName: String,
@field:NotBlank(message = "Staff emergContactPhone cannot be empty")
val emergContactPhone: String,
@field:NotBlank(message = "Staff employType cannot be empty")
val employType: String,

val id: Long?,
val skillSetId: List<Long>?,
val gradeId: Long?,
val phone2: String?,
val teamId: Long?,
val departDate: LocalDate?,
val departReason: String?,
val remark: String?,

+ 10
- 0
src/main/java/com/ffii/tsms/modules/project/entity/Project.kt Voir le fichier

@@ -108,4 +108,14 @@ open class Project : BaseEntity<Long>() {
inverseJoinColumns = [JoinColumn(name = "workNaturesId")]
)
open var workNatures: MutableSet<WorkNature> = mutableSetOf()

@ManyToOne
@JoinColumn(name = "taskTemplateId")
open var taskTemplate: TaskTemplate? = null

@Column(name = "status")
open var status: String? = null

@Column(name = "isClpProject")
open var isClpProject: Boolean? = null
}

+ 3
- 0
src/main/java/com/ffii/tsms/modules/project/entity/ProjectRepository.kt Voir le fichier

@@ -4,6 +4,7 @@ import com.ffii.core.support.AbstractRepository
import com.ffii.tsms.modules.project.entity.projections.InvoiceInfoSearchInfo
import com.ffii.tsms.modules.project.entity.projections.InvoiceSearchInfo
import com.ffii.tsms.modules.project.entity.projections.ProjectSearchInfo
import java.io.Serializable

interface ProjectRepository : AbstractRepository<Project, Long> {
fun findProjectSearchInfoByOrderByCreatedDesc(): List<ProjectSearchInfo>
@@ -13,4 +14,6 @@ interface ProjectRepository : AbstractRepository<Project, Long> {
fun findInvoiceSearchInfoById(id: Long): List<InvoiceSearchInfo>

fun findInvoiceInfoSearchInfoById(id: Long): List<InvoiceInfoSearchInfo>

fun findFirstByIsClpProjectAndIdNotOrderByIdDesc(isClpProject: Boolean, id: Serializable?): Project?
}

+ 1
- 0
src/main/java/com/ffii/tsms/modules/project/entity/projections/ProjectSearchInfo.kt Voir le fichier

@@ -9,6 +9,7 @@ interface ProjectSearchInfo {
val id: Long?
val name: String?
val code: String?
val status: String?

@get:Value("#{target.projectCategory.name}")
val category: String?


+ 128
- 61
src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt Voir le fichier

@@ -40,8 +40,11 @@ open class ProjectsService(
private val milestoneRepository: MilestoneRepository,
private val gradeAllocationRepository: GradeAllocationRepository,
private val projectTaskRepository: ProjectTaskRepository,
private val milestonePaymentRepository: MilestonePaymentRepository, private val taskGroupRepository: TaskGroupRepository,
private val timesheetRepository: TimesheetRepository
private val milestonePaymentRepository: MilestonePaymentRepository,
private val taskGroupRepository: TaskGroupRepository,
private val timesheetRepository: TimesheetRepository,
private val taskTemplateRepository: TaskTemplateRepository,
private val subsidiaryRepository: SubsidiaryRepository, private val subsidiaryContactRepository: SubsidiaryContactRepository
) {
open fun allProjects(): List<ProjectSearchInfo> {
return projectRepository.findProjectSearchInfoByOrderByCreatedDesc()
@@ -60,34 +63,42 @@ open class ProjectsService(
}

open fun markDeleted(id: Long) {
projectRepository.save(projectRepository.findById(id).orElseThrow().apply { deleted = true })
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
)
} }
.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()
}
@@ -102,10 +113,12 @@ open class ProjectsService(
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)
) }
.mapValues { (_, milestone) ->
MilestoneInfo(
startDate = milestone.startDate?.format(DateTimeFormatter.ISO_LOCAL_DATE),
endDate = milestone.endDate?.format(DateTimeFormatter.ISO_LOCAL_DATE)
)
}
)
}
}
@@ -114,6 +127,19 @@ open class ProjectsService(
return projectCategoryRepository.findAll()
}

open fun createProjectCode(isClpProject: Boolean?, project: Project): String {
val checkIsClpProject = isClpProject != null && isClpProject
val prefix = if (checkIsClpProject) "C" else "M"

val latestProjectCode = projectRepository.findFirstByIsClpProjectAndIdNotOrderByIdDesc(checkIsClpProject, project.id)

if (latestProjectCode != null) {
val lastFix = latestProjectCode.code!!.split("-")[1] //C-0001 -> 0001
return "$prefix-" + String.format("%04d", lastFix.toLong() + 1L)
} else {
return "$prefix-0001"
}
}
@Transactional
open fun saveProject(request: NewProjectRequest): NewProjectResponse {
val projectCategory =
@@ -124,9 +150,12 @@ open class ProjectsService(
val location = locationRepository.findById(request.locationId).orElseThrow()
val buildingTypes = buildingTypeRepository.findAllById(request.buildingTypeIds).toMutableSet()
val workNatures = workNatureRepository.findAllById(request.workNatureIds).toMutableSet()

val taskTemplate =
if (request.taskTemplateId != null && request.taskTemplateId > 0) taskTemplateRepository.findById(request.taskTemplateId)
.orElseThrow() else null
val teamLead = staffRepository.findById(request.projectLeadId).orElseThrow()
val customer = customerService.findCustomer(request.clientId)
val subsidiaryContact = subsidiaryContactRepository.findById(request.clientContactId).orElse(null)
val clientContact = customerContactService.findByContactId(request.clientContactId)
val customerSubsidiary = request.clientSubsidiaryId?.let { subsidiaryService.findSubsidiary(it) }

@@ -134,15 +163,24 @@ open class ProjectsService(
val taskGroupMap = tasksService.allTaskGroups().associateBy { it.id }
val gradeMap = gradeService.allGrades().associateBy { it.id }

val project = if (request.projectId != null && request.projectId > 0) projectRepository.findById(request.projectId).orElseThrow() else Project()
val project =
if (request.projectId != null && request.projectId > 0) projectRepository.findById(request.projectId)
.orElseThrow() else Project()
project.apply {
name = request.projectName
description = request.projectDescription
code = request.projectCode
println(this.code)
println(this.code.isNullOrEmpty())
code = if (this.code.isNullOrEmpty()) createProjectCode(request.isClpProject, project) else this.code
expectedTotalFee = request.expectedProjectFee
totalManhour = request.totalManhour
actualStart = request.projectActualStart
actualEnd = request.projectActualEnd
status = if (this.status == "Deleted" || this.deleted == true) "Deleted"
else if (this.actualStart != null && this.actualEnd != null) "Completed"
else if (this.actualStart != null) "On-going"
else "Pending To Start"
isClpProject = request.isClpProject

this.projectCategory = projectCategory
this.fundingType = fundingType
@@ -151,12 +189,13 @@ open class ProjectsService(
this.location = location
this.buildingTypes = buildingTypes
this.workNatures = workNatures
this.taskTemplate = taskTemplate

this.teamLead = teamLead
this.customer = customer
custLeadName = clientContact.name
custLeadEmail = clientContact.email
custLeadPhone = clientContact.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
}

@@ -165,7 +204,10 @@ open class ProjectsService(
val tasksToSave = mutableListOf<ProjectTask>()
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) ?: Milestone() else Milestone()
val milestone = if (project.id != null && project.id!! > 0L) milestoneRepository.findByProjectAndTaskGroup(
project,
taskGroup
) ?: Milestone() else Milestone()
milestone.apply {
val newMilestone = this
val requestMilestone = request.milestones[taskStageId]
@@ -177,7 +219,8 @@ open class ProjectsService(
this.milestonePayments.removeAll(this.milestonePayments)
}
requestMilestone?.payments?.map {
val milestonePayment = milestonePaymentRepository.findById(it.id).orElse(MilestonePayment()) ?:MilestonePayment()
val milestonePayment =
milestonePaymentRepository.findById(it.id).orElse(MilestonePayment()) ?: MilestonePayment()
this.milestonePayments.add(milestonePayment.apply {
this.milestone = newMilestone
this.description = it.description
@@ -190,7 +233,11 @@ open class ProjectsService(
this.stagePercentAllocation = taskGroupAllocation.percentAllocation

taskGroupAllocation.taskIds.map { taskId ->
val projectTask = if (project.id != null && project.id!! > 0L) projectTaskRepository.findByProjectAndTask(project, allTasksMap[taskId]!!) ?:ProjectTask() else ProjectTask()
val projectTask =
if (project.id != null && project.id!! > 0L) projectTaskRepository.findByProjectAndTask(
project,
allTasksMap[taskId]!!
) ?: ProjectTask() else ProjectTask()

projectTask.apply {
this.project = project
@@ -205,7 +252,11 @@ open class ProjectsService(

// Grade allocation (from manhourPercentageByGrade)
val gradeAllocations = request.manhourPercentageByGrade.entries.map {
val gradeAllocation = if (project.id != null && project.id!! > 0L) gradeAllocationRepository.findByProjectAndGrade(project, gradeMap[it.key]!!) ?: GradeAllocation() else GradeAllocation()
val gradeAllocation =
if (project.id != null && project.id!! > 0L) gradeAllocationRepository.findByProjectAndGrade(
project,
gradeMap[it.key]!!
) ?: GradeAllocation() else GradeAllocation()

gradeAllocation.apply {
this.project = project
@@ -225,7 +276,8 @@ open class ProjectsService(

val milestonesToDelete = milestoneRepository.findAllByProject(project).subtract(milestones.toSet())
val tasksToDelete = projectTaskRepository.findAllByProject(project).subtract(tasksToSave.toSet())
val gradeAllocationsToDelete = gradeAllocationRepository.findByProject(project).subtract(gradeAllocations.toSet())
val gradeAllocationsToDelete =
gradeAllocationRepository.findByProject(project).subtract(gradeAllocations.toSet())
milestoneRepository.deleteAll(milestonesToDelete)
projectTaskRepository.deleteAll(tasksToDelete)
gradeAllocationRepository.deleteAll(gradeAllocationsToDelete)
@@ -236,11 +288,14 @@ open class ProjectsService(

// Staff allocation
val allocatedStaff = staffRepository.findAllById(request.allocatedStaffIds)
val staffAllocations = allocatedStaff.map { staff -> StaffAllocation().apply {
this.project = savedProject
this.staff = staff
} }
val staffAllocationsToDelete = staffAllocationRepository.findByProject(savedProject).subtract(staffAllocations.toSet())
val staffAllocations = allocatedStaff.map { staff ->
StaffAllocation().apply {
this.project = savedProject
this.staff = staff
}
}
val staffAllocationsToDelete =
staffAllocationRepository.findByProject(savedProject).subtract(staffAllocations.toSet())
staffAllocationRepository.deleteAll(staffAllocationsToDelete)
staffAllocationRepository.saveAll(staffAllocations)

@@ -260,7 +315,9 @@ open class ProjectsService(
val project = projectRepository.findById(projectId)

return project.getOrNull()?.let {
val customerContact = it.customer?.id?.let { customerId -> customerContactService.findAllByCustomerId(customerId) } ?: emptyList()
val customerContact =
it.customer?.id?.let { customerId -> customerContactService.findAllByCustomerId(customerId) }
?: emptyList()

val milestoneMap = it.milestones
.filter { milestone -> milestone.taskGroup?.id != null }
@@ -276,11 +333,14 @@ open class ProjectsService(
projectLeadId = it.teamLead?.id,
projectActualStart = it.actualStart,
projectActualEnd = it.actualEnd,
projectStatus = it.status,
isClpProject = it.isClpProject,
serviceTypeId = it.serviceType?.id,
fundingTypeId = it.fundingType?.id,
contractTypeId = it.contractType?.id,
locationId = it.location?.id,
buildingTypeIds = it.buildingTypes.mapNotNull { buildingType -> buildingType.id },
taskTemplateId = it.taskTemplate?.id,
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,
@@ -292,21 +352,28 @@ open class ProjectsService(
taskGroups = projectTaskRepository.findAllByProject(it)
.mapNotNull { projectTask -> if (projectTask.task?.taskGroup?.id != null) projectTask.task else null }
.groupBy { task -> task.taskGroup!!.id!! }
.mapValues { (taskGroupId, tasks) -> TaskGroupAllocation(
taskIds = tasks.mapNotNull { task -> task.id },
percentAllocation = milestoneMap[taskGroupId]?.stagePercentAllocation ?: 0.0
) },
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),
endDate = milestone.endDate?.format(DateTimeFormatter.ISO_LOCAL_DATE),
payments = milestone.milestonePayments.map { payment -> PaymentInputs(
id = payment.id!!,
amount = payment.amount!!,
description = payment.description!!,
date = payment.date!!.format(DateTimeFormatter.ISO_LOCAL_DATE)
)}
)},
.mapValues { (taskGroupId, tasks) ->
TaskGroupAllocation(
taskIds = tasks.mapNotNull { task -> task.id },
percentAllocation = milestoneMap[taskGroupId]?.stagePercentAllocation ?: 0.0
)
},
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),
endDate = milestone.endDate?.format(DateTimeFormatter.ISO_LOCAL_DATE),
payments = milestone.milestonePayments.map { payment ->
PaymentInputs(
id = payment.id!!,
amount = payment.amount!!,
description = payment.description!!,
date = payment.date!!.format(DateTimeFormatter.ISO_LOCAL_DATE)
)
}
)
},
expectedProjectFee = it.expectedTotalFee
)
}


+ 3
- 0
src/main/java/com/ffii/tsms/modules/project/web/models/EditProjectDetails.kt Voir le fichier

@@ -13,6 +13,8 @@ data class EditProjectDetails(
val projectLeadId: Long?,
val projectActualStart: LocalDate?,
val projectActualEnd: LocalDate?,
val projectStatus: String?,
val isClpProject: Boolean?,

val serviceTypeId: Long?,
val fundingTypeId: Long?,
@@ -20,6 +22,7 @@ data class EditProjectDetails(
val locationId: Long?,
val buildingTypeIds: List<Long>,
val workNatureIds: List<Long>,
val taskTemplateId: Long?,

// Client details
val clientId: Long?,


+ 6
- 2
src/main/java/com/ffii/tsms/modules/project/web/models/NewProjectRequest.kt Voir le fichier

@@ -1,12 +1,13 @@
package com.ffii.tsms.modules.project.web.models

import com.ffii.tsms.modules.data.entity.SubsidiaryContact
import jakarta.validation.constraints.NotBlank
import java.time.LocalDate

data class NewProjectRequest(
// Project details
@field:NotBlank(message = "project code cannot be empty")
val projectCode: String,
// @field:NotBlank(message = "project code cannot be empty")
// val projectCode: String,
@field:NotBlank(message = "project name cannot be empty")
val projectName: String,
val projectCategoryId: Long,
@@ -15,6 +16,7 @@ data class NewProjectRequest(
val projectId: Long?,
val projectActualStart: LocalDate?,
val projectActualEnd: LocalDate?,
val isClpProject: Boolean?,

val serviceTypeId: Long,
val fundingTypeId: Long,
@@ -22,6 +24,8 @@ data class NewProjectRequest(
val locationId: Long,
val buildingTypeIds: List<Long>,
val workNatureIds: List<Long>,
val taskTemplateId: Long?,
val isSubsidiaryContact: Boolean?,

// Client details
val clientId: Long,


+ 83
- 1
src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt Voir le fichier

@@ -16,6 +16,7 @@ import java.io.IOException
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.util.*
import org.apache.poi.ss.util.CellAddress

data class DayInfo(val date: String?, val weekday: String?)
@Service
@@ -27,6 +28,7 @@ open class ReportService {
private val PROJECT_CASH_FLOW_REPORT = "templates/report/EX02_Project Cash Flow 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 LATE_START_REPORT = "templates/report/AR01_Late Start Report v01.xlsx"

// ==============================|| GENERATE REPORT ||============================== //

@@ -78,6 +80,15 @@ open class ReportService {
return outputStream.toByteArray()
}

@Throws(IOException::class)
fun generateLateStartReport(project: Project): ByteArray {
val workbook: Workbook = createLateStartReport(project,LATE_START_REPORT)
val outputStream: ByteArrayOutputStream = ByteArrayOutputStream()
workbook.write(outputStream)
workbook.close()
return outputStream.toByteArray()
}

// ==============================|| CREATE REPORT ||============================== //

// EX01 Financial Report
@@ -106,7 +117,7 @@ open class ReportService {
val sheet: Sheet = workbook.getSheetAt(0)

// accounting style + comma style
val accountingStyle = workbook.createDataFormat().getFormat("_(* #,##0.00_);_(* (#,##0.00);_(* \"-\"??_);_(@_)")
val accountingStyle = workbook.createDataFormat() .getFormat("_(* #,##0.00_);_(* (#,##0.00);_(* \"-\"??_);_(@_)")

var rowIndex = 1 // Assuming the location is in (1,2), which is the report date field
var columnIndex = 2
@@ -552,4 +563,75 @@ open class ReportService {
return workbook
}

private fun createLateStartReport(
project: Project,
templatePath: String
):Workbook{

project
val resource = ClassPathResource(templatePath)
val templateInputStream = resource.inputStream
val workbook: Workbook = XSSFWorkbook(templateInputStream)
val sheet = workbook.getSheetAt(0)
// Formatting the current date to "YYYY/MM/DD" and setting it to cell C2
val formattedToday = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"))
val dateCell = sheet.getRow(1)?.getCell(2) ?: sheet.getRow(1).createCell(2)
dateCell.setCellValue(formattedToday)
// Styling for cell A1: Font size 16 and bold
val headerFont = workbook.createFont().apply {
bold = true
fontHeightInPoints = 16
}
val headerCellStyle = workbook.createCellStyle().apply {
setFont(headerFont)
}
val headerCell = sheet.getRow(0)?.getCell(0) ?: sheet.getRow(0).createCell(0)
headerCell.cellStyle = headerCellStyle
headerCell.setCellValue("Report Title")
// Apply styles from A2 to A4 (bold)
val boldFont = workbook.createFont().apply { bold = true }
val boldCellStyle = workbook.createCellStyle().apply { setFont(boldFont) }
listOf(1, 2, 3).forEach { rowIndex ->
val row = sheet.getRow(rowIndex)
val cell = row?.getCell(0) ?: row.createCell(0)
cell.cellStyle = boldCellStyle
}
// Apply styles from A6 to J6 (bold, bottom border, center alignment)
val styleA6ToJ6 = workbook.createCellStyle().apply {
setFont(boldFont)
alignment = HorizontalAlignment.CENTER
borderBottom = BorderStyle.THIN
}
for (colIndex in 0..9) {
val cellAddress = CellAddress(5, colIndex) // Row 6 (0-based index), Columns A to J
val row = sheet.getRow(cellAddress.row)
val cell = row?.getCell(cellAddress.column) ?: row.createCell(cellAddress.column)
cell.cellStyle = styleA6ToJ6
}
// Setting column widths dynamically based on content length (example logic)
val maxContentWidths = IntArray(10) { 8 } // Initial widths for A to J
for (rowIndex in 0..sheet.lastRowNum) {
val row = sheet.getRow(rowIndex)
for (colIndex in 0..9) {
val cell = row.getCell(colIndex)
if (cell != null) {
val length = cell.toString().length
if (length > maxContentWidths[colIndex]) {
maxContentWidths[colIndex] = length
}
}
}
}
for (colIndex in 0..9) {
sheet.setColumnWidth(colIndex, (maxContentWidths[colIndex] + 2) * 256) // Set the width for each column
}
return workbook
}

}

+ 16
- 0
src/main/java/com/ffii/tsms/modules/report/web/ReportController.kt Voir le fichier

@@ -22,6 +22,8 @@ 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 java.io.IOException
import java.time.LocalDate

@@ -93,4 +95,18 @@ class ReportController(
val project = projectRepository.findById(id).orElseThrow()
return invoiceService.findAllByProjectAndPaidAmountIsNotNull(project)
}

@PostMapping("/downloadLateStartReport")
fun downloadLateStartReport(): ResponseEntity<ByteArrayResource> {
val reportBytes = excelReportService.generateLateStartReport()
val headers = HttpHeaders()
headers.add("Content-Disposition", "attachment; filename=Late_Start_Report_${LocalDate.now()}.xlsx")

return ResponseEntity.ok()
.headers(headers)
.contentLength(reportBytes.size.toLong())
.contentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"))
.body(ByteArrayResource(reportBytes))
}

}

+ 13
- 0
src/main/resources/db/changelog/changes/20240513_01_cyril/01_update_project.sql Voir le fichier

@@ -0,0 +1,13 @@
-- liquibase formatted sql
-- changeset cyril:project

ALTER TABLE `project`
ADD COLUMN `taskTemplateId` INT NULL DEFAULT NULL AFTER `customerSubsidiaryId`,
ADD INDEX `FK_PROJECT_ON_TASKTEMPLATEID` (`taskTemplateId` ASC) VISIBLE;
;
ALTER TABLE `project`
ADD CONSTRAINT `FK_PROJECT_ON_TASKTEMPLATEID`
FOREIGN KEY (`taskTemplateId`)
REFERENCES `task_template` (`id`)
ON DELETE NO ACTION
ON UPDATE NO ACTION;

+ 5
- 0
src/main/resources/db/changelog/changes/20240513_01_cyril/02_update_project.sql Voir le fichier

@@ -0,0 +1,5 @@
-- liquibase formatted sql
-- changeset cyril:project

ALTER TABLE `project`
ADD COLUMN `status` VARCHAR(40) NULL DEFAULT 'Pending to Start' AFTER `taskTemplateId`;

+ 5
- 0
src/main/resources/db/changelog/changes/20240514_01_cyril/01_update_project.sql Voir le fichier

@@ -0,0 +1,5 @@
-- liquibase formatted sql
-- changeset cyril:project

ALTER TABLE `project`
ADD COLUMN `isClpProject` TINYINT NULL DEFAULT 0 AFTER `status`;

Chargement…
Annuler
Enregistrer