| @@ -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; | |||
| } | |||
| } | |||
| @@ -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; | |||
| @@ -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); | |||
| } | |||
| @@ -22,4 +22,7 @@ interface StaffSearchInfo { | |||
| @get:Value("#{target.currentPosition?.name}") | |||
| val currentPosition: String? | |||
| @get:Value("#{target.user?.id}") | |||
| val userId: Long? | |||
| } | |||
| @@ -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() | |||
| @@ -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 | |||
| @@ -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>> { | |||
| @@ -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?, | |||
| @@ -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 | |||
| } | |||
| @@ -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? | |||
| } | |||
| @@ -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? | |||
| @@ -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 | |||
| ) | |||
| } | |||
| @@ -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?, | |||
| @@ -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, | |||
| @@ -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 | |||
| } | |||
| } | |||
| @@ -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)) | |||
| } | |||
| } | |||
| @@ -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; | |||
| @@ -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`; | |||
| @@ -0,0 +1,5 @@ | |||
| -- liquibase formatted sql | |||
| -- changeset cyril:project | |||
| ALTER TABLE `project` | |||
| ADD COLUMN `isClpProject` TINYINT NULL DEFAULT 0 AFTER `status`; | |||