From 9ddfcf6aab466ebe691b9537b712b45eefea1224 Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Wed, 19 Jun 2024 15:01:35 +0800 Subject: [PATCH] add project import function --- .../data/entity/BuildingTypeRepository.kt | 1 + .../data/entity/CustomerRepository.java | 4 + .../modules/data/entity/LocationRepository.kt | 2 + .../modules/data/entity/StaffRepository.java | 1 + .../tsms/modules/data/entity/Subsidiary.java | 1 + .../data/entity/SubsidiaryRepository.java | 6 + .../modules/data/entity/TeamRepository.java | 2 + .../data/entity/WorkNatureRepository.kt | 1 + .../modules/data/service/CustomerService.kt | 12 + .../modules/data/service/SubsidiaryService.kt | 11 + .../project/entity/ProjectRepository.kt | 2 + .../project/service/ProjectsService.kt | 294 ++++++++++++++++-- .../modules/project/web/ProjectsController.kt | 26 +- .../project/web/models/NewProjectRequest.kt | 6 +- .../20240617_01_cyril/01_update_task.sql | 9 + .../20240617_01_cyril/02_update_project.sql | 5 + 16 files changed, 355 insertions(+), 28 deletions(-) create mode 100644 src/main/resources/db/changelog/changes/20240617_01_cyril/01_update_task.sql create mode 100644 src/main/resources/db/changelog/changes/20240617_01_cyril/02_update_project.sql diff --git a/src/main/java/com/ffii/tsms/modules/data/entity/BuildingTypeRepository.kt b/src/main/java/com/ffii/tsms/modules/data/entity/BuildingTypeRepository.kt index 1a4443b..336dc1c 100644 --- a/src/main/java/com/ffii/tsms/modules/data/entity/BuildingTypeRepository.kt +++ b/src/main/java/com/ffii/tsms/modules/data/entity/BuildingTypeRepository.kt @@ -3,4 +3,5 @@ package com.ffii.tsms.modules.data.entity; import com.ffii.core.support.AbstractRepository interface BuildingTypeRepository : AbstractRepository { + fun findByName(name: String): BuildingType? } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/data/entity/CustomerRepository.java b/src/main/java/com/ffii/tsms/modules/data/entity/CustomerRepository.java index d075a18..58355b6 100644 --- a/src/main/java/com/ffii/tsms/modules/data/entity/CustomerRepository.java +++ b/src/main/java/com/ffii/tsms/modules/data/entity/CustomerRepository.java @@ -3,6 +3,7 @@ package com.ffii.tsms.modules.data.entity; import java.util.Optional; import java.util.List; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import com.ffii.core.support.AbstractRepository; @@ -11,4 +12,7 @@ public interface CustomerRepository extends AbstractRepository { List findAllByDeletedFalse(); Optional findByCode(@Param("code") String code); Optional findByName(@Param("name") String name); + + @Query("SELECT max(cast(substring_index(c.code, '-', -1) as long)) FROM Customer c") + Long getLatestCodeNumber(); } diff --git a/src/main/java/com/ffii/tsms/modules/data/entity/LocationRepository.kt b/src/main/java/com/ffii/tsms/modules/data/entity/LocationRepository.kt index 278bd35..6ad53c3 100644 --- a/src/main/java/com/ffii/tsms/modules/data/entity/LocationRepository.kt +++ b/src/main/java/com/ffii/tsms/modules/data/entity/LocationRepository.kt @@ -3,4 +3,6 @@ package com.ffii.tsms.modules.data.entity; import com.ffii.core.support.AbstractRepository interface LocationRepository : AbstractRepository { + + fun findByName(name: String): Location } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/data/entity/StaffRepository.java b/src/main/java/com/ffii/tsms/modules/data/entity/StaffRepository.java index b9fc459..f6e8a46 100644 --- a/src/main/java/com/ffii/tsms/modules/data/entity/StaffRepository.java +++ b/src/main/java/com/ffii/tsms/modules/data/entity/StaffRepository.java @@ -25,4 +25,5 @@ public interface StaffRepository extends AbstractRepository { Optional> findAllByDeletedFalse(); Optional findIdAndNameByUserIdAndDeletedFalse(Long id); + } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/data/entity/Subsidiary.java b/src/main/java/com/ffii/tsms/modules/data/entity/Subsidiary.java index 980a8ba..725b475 100644 --- a/src/main/java/com/ffii/tsms/modules/data/entity/Subsidiary.java +++ b/src/main/java/com/ffii/tsms/modules/data/entity/Subsidiary.java @@ -10,6 +10,7 @@ import java.util.List; import static jakarta.persistence.CascadeType.ALL; + @Entity @Table(name = "subsidiary") public class Subsidiary extends BaseEntity { diff --git a/src/main/java/com/ffii/tsms/modules/data/entity/SubsidiaryRepository.java b/src/main/java/com/ffii/tsms/modules/data/entity/SubsidiaryRepository.java index d94b9b9..9edc95c 100644 --- a/src/main/java/com/ffii/tsms/modules/data/entity/SubsidiaryRepository.java +++ b/src/main/java/com/ffii/tsms/modules/data/entity/SubsidiaryRepository.java @@ -1,6 +1,7 @@ package com.ffii.tsms.modules.data.entity; import com.ffii.core.support.AbstractRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import java.util.List; @@ -12,4 +13,9 @@ public interface SubsidiaryRepository extends AbstractRepository findAllByDeletedFalseAndIdIn(List id); Optional findByCode(@Param("code") String code); + + Optional findByName(@Param("name") String name); + + @Query("SELECT max(cast(substring_index(s.code, '-', -1) as long)) FROM Subsidiary s") + Long getLatestCodeNumber(); } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/data/entity/TeamRepository.java b/src/main/java/com/ffii/tsms/modules/data/entity/TeamRepository.java index 3a3cf41..a788348 100644 --- a/src/main/java/com/ffii/tsms/modules/data/entity/TeamRepository.java +++ b/src/main/java/com/ffii/tsms/modules/data/entity/TeamRepository.java @@ -9,4 +9,6 @@ public interface TeamRepository extends AbstractRepository { List findByDeletedFalse(); Team findByStaff(Staff staff); + + Team findByCode(String code); } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/data/entity/WorkNatureRepository.kt b/src/main/java/com/ffii/tsms/modules/data/entity/WorkNatureRepository.kt index 24f88c7..20144a5 100644 --- a/src/main/java/com/ffii/tsms/modules/data/entity/WorkNatureRepository.kt +++ b/src/main/java/com/ffii/tsms/modules/data/entity/WorkNatureRepository.kt @@ -3,4 +3,5 @@ package com.ffii.tsms.modules.data.entity; import com.ffii.core.support.AbstractRepository interface WorkNatureRepository : AbstractRepository { + fun findByName(name: String): WorkNature? } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/data/service/CustomerService.kt b/src/main/java/com/ffii/tsms/modules/data/service/CustomerService.kt index 4abb66e..9202f2d 100644 --- a/src/main/java/com/ffii/tsms/modules/data/service/CustomerService.kt +++ b/src/main/java/com/ffii/tsms/modules/data/service/CustomerService.kt @@ -37,6 +37,18 @@ open class CustomerService( return customerRepository.findByCode(code); } + open fun createClientCode(): String { + val prefix = "CT" + + val latestClientCode = customerRepository.getLatestCodeNumber() + + if (latestClientCode != null) { + return "$prefix-" + String.format("%03d", latestClientCode + 1L) + } else { + return "$prefix-001" + } + } + open fun saveCustomer(saveCustomer: SaveCustomerRequest): SaveCustomerResponse { val duplicateCustomer = findCustomerByCode(saveCustomer.code) diff --git a/src/main/java/com/ffii/tsms/modules/data/service/SubsidiaryService.kt b/src/main/java/com/ffii/tsms/modules/data/service/SubsidiaryService.kt index ea55431..23f53c6 100644 --- a/src/main/java/com/ffii/tsms/modules/data/service/SubsidiaryService.kt +++ b/src/main/java/com/ffii/tsms/modules/data/service/SubsidiaryService.kt @@ -38,6 +38,17 @@ open class SubsidiaryService( return subsidiaryRepository.findByCode(code); } + open fun createSubsidiaryCode(): String { + val prefix = "SY" + + val latestSubsidiaryCode = subsidiaryRepository.getLatestCodeNumber() + + if (latestSubsidiaryCode != null) { + return "$prefix-" + String.format("%03d", latestSubsidiaryCode + 1L) + } else { + return "$prefix-001" + } + } open fun saveSubsidiary(saveSubsidiary: SaveSubsidiaryRequest): SaveSubsidiaryResponse { val duplicateSubsidiary = findSubsidiaryByCode(saveSubsidiary.code) diff --git a/src/main/java/com/ffii/tsms/modules/project/entity/ProjectRepository.kt b/src/main/java/com/ffii/tsms/modules/project/entity/ProjectRepository.kt index 8f367a8..4fdf2dc 100644 --- a/src/main/java/com/ffii/tsms/modules/project/entity/ProjectRepository.kt +++ b/src/main/java/com/ffii/tsms/modules/project/entity/ProjectRepository.kt @@ -34,4 +34,6 @@ interface ProjectRepository : AbstractRepository { fun findAllByStatusIsNotAndMainProjectIsNull(status: String): List fun findAllByTeamLeadAndCustomer(teamLead: Staff, customer: Customer): List + + fun findByCode(code: String): Project? } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt b/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt index dfb9bbe..1b69735 100644 --- a/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt +++ b/src/main/java/com/ffii/tsms/modules/project/service/ProjectsService.kt @@ -1,26 +1,26 @@ package com.ffii.tsms.modules.project.service +import com.ffii.core.utils.ExcelUtils import com.ffii.tsms.modules.common.SecurityUtils import com.ffii.tsms.modules.data.entity.* -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.data.service.* import com.ffii.tsms.modules.project.entity.* import com.ffii.tsms.modules.project.entity.Milestone import com.ffii.tsms.modules.project.entity.projections.InvoiceInfoSearchInfo import com.ffii.tsms.modules.project.entity.projections.InvoiceSearchInfo +import com.ffii.tsms.modules.project.entity.projections.ProjectSearchInfo import com.ffii.tsms.modules.project.web.models.* import com.ffii.tsms.modules.timesheet.entity.TimesheetRepository +import org.apache.commons.logging.LogFactory +import org.apache.poi.ss.usermodel.Sheet +import org.apache.poi.ss.usermodel.Workbook import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import java.time.LocalDate import java.time.format.DateTimeFormatter -import java.util.Optional -import kotlin.jvm.optionals.getOrElse import kotlin.jvm.optionals.getOrNull + @Service open class ProjectsService( private val projectRepository: ProjectRepository, @@ -46,7 +46,11 @@ open class ProjectsService( private val timesheetRepository: TimesheetRepository, private val taskTemplateRepository: TaskTemplateRepository, private val subsidiaryContactService: SubsidiaryContactService, - private val subsidiaryContactRepository: SubsidiaryContactRepository + private val subsidiaryContactRepository: SubsidiaryContactRepository, + private val teamRepository: TeamRepository, + private val customerRepository: CustomerRepository, + private val subsidiaryRepository: SubsidiaryRepository, + private val customerSubsidiaryService: CustomerSubsidiaryService, ) { open fun allProjects(): List { return projectRepository.findProjectSearchInfoByOrderByCreatedDesc() @@ -164,10 +168,15 @@ open class ProjectsService( .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 subsidiaryContact = + if (request.clientContactId != null) subsidiaryContactRepository.findById(request.clientContactId) + .orElse(null) else null + val clientContact = + if (request.clientContactId != null) customerContactService.findByContactId(request.clientContactId) else null 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 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 } @@ -179,15 +188,23 @@ open class ProjectsService( project.apply { name = request.projectName description = request.projectDescription - 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 + code = request.projectCode + ?: 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 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" + status = request.projectStatus + ?: 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.mainProject = mainProject @@ -203,11 +220,11 @@ open class ProjectsService( this.teamLead = teamLead this.customer = customer custLeadName = - if (request.isSubsidiaryContact == null || !request.isSubsidiaryContact) clientContact.name else subsidiaryContact.name + if (request.isSubsidiaryContact == null || !request.isSubsidiaryContact) clientContact?.name else subsidiaryContact?.name custLeadEmail = - if (request.isSubsidiaryContact == null || !request.isSubsidiaryContact) clientContact.email else subsidiaryContact.email + if (request.isSubsidiaryContact == null || !request.isSubsidiaryContact) clientContact?.email else subsidiaryContact?.email custLeadPhone = - if (request.isSubsidiaryContact == null || !request.isSubsidiaryContact) clientContact.phone else subsidiaryContact.phone + 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 @@ -279,11 +296,11 @@ open class ProjectsService( if (milestones.isNotEmpty()) { project.apply { planStart = milestones.mapNotNull { it.startDate }.minOrNull() - planEnd = milestones.mapNotNull { it.endDate }.maxOrNull() + planEnd = request.projectPlanEnd ?: milestones.mapNotNull { it.endDate }.maxOrNull() } } - val savedProject = projectRepository.save(project) + val savedProject = projectRepository.saveAndFlush(project) val milestonesToDelete = milestoneRepository.findAllByProject(project).subtract(milestones.toSet()) val tasksToDelete = projectTaskRepository.findAllByProject(project).subtract(tasksToSave.toSet()) @@ -422,8 +439,9 @@ open class ProjectsService( val subsidiaryContact = project.customerSubsidiary?.id?.let { subsidiaryId -> subsidiaryContactService.findAllBySubsidiaryId(subsidiaryId) } ?: emptyList() - val customerContact = project.customer?.id?.let { customerId -> customerContactService.findAllByCustomerId(customerId) } - ?: emptyList() + val customerContact = + project.customer?.id?.let { customerId -> customerContactService.findAllByCustomerId(customerId) } + ?: emptyList() MainProjectDetails( projectId = project.id, @@ -441,10 +459,238 @@ open class ProjectsService( 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, + clientContactId = subsidiaryContact.find { contact -> contact.name == project.custLeadName }?.id + ?: customerContact.find { contact -> contact.name == project.custLeadName }?.id, clientSubsidiaryId = project.customerSubsidiary?.id, expectedProjectFee = project.expectedTotalFee ) } } + + @Transactional(rollbackFor = [Exception::class]) + open fun importFile(workbook: Workbook?): String { + val logger = LogFactory.getLog(javaClass) + + if (workbook == null) { + return "No Excel import" // if workbook is null + } + + val sheet: Sheet = workbook.getSheetAt(0) + + for (i in 2..() + for (j in 18.. 0, + manhourPercentageByGrade = taskTemplate.gradeAllocations.associateBy( + keySelector = { it.grade!!.id!! }, + valueTransform = { it.percentage!! } + ), + projectActualEnd = null, + taskTemplateId = taskTemplate.id, + taskGroups = mapOf(Pair(5, TaskGroupAllocation(mutableListOf(41), 100.0))), + milestones = mapOf( + Pair( + 5, com.ffii.tsms.modules.project.web.models.Milestone( + startDate = ExcelUtils.getDateValue( + row.getCell(2), + DateTimeFormatter.ofPattern("MM/dd/yyyy") + ).format(formatter), + endDate = ExcelUtils.getDateValue( + row.getCell(3), + DateTimeFormatter.ofPattern("MM/dd/yyyy") + ).format(formatter), + payments = mutableListOf( + PaymentInputs( + -1, "Manhour Import", ExcelUtils.getDateValue( + row.getCell(2), + DateTimeFormatter.ofPattern("MM/dd/yyyy") + ).toString(), row.getCell(9).numericCellValue + ) + ) + ) + ) + )) + ) + } + } + + return if (sheet.lastRowNum > 0) "Import Excel success" else "Import Excel failure" + } } diff --git a/src/main/java/com/ffii/tsms/modules/project/web/ProjectsController.kt b/src/main/java/com/ffii/tsms/modules/project/web/ProjectsController.kt index 447a50b..61c070e 100644 --- a/src/main/java/com/ffii/tsms/modules/project/web/ProjectsController.kt +++ b/src/main/java/com/ffii/tsms/modules/project/web/ProjectsController.kt @@ -2,15 +2,21 @@ 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.entity.projections.ProjectSearchInfo import com.ffii.tsms.modules.project.service.ProjectsService import com.ffii.tsms.modules.project.web.models.* +import jakarta.servlet.http.HttpServletRequest import jakarta.validation.Valid +import org.apache.poi.ss.usermodel.Workbook +import org.apache.poi.xssf.usermodel.XSSFWorkbook import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.ServletRequestBindingException import org.springframework.web.bind.annotation.* +import org.springframework.web.multipart.MultipartHttpServletRequest + @RestController @RequestMapping("/projects") @@ -85,4 +91,20 @@ class ProjectsController(private val projectsService: ProjectsService, private v fun projectWorkNatures(): List { return projectsService.allWorkNatures() } + + @PostMapping("/import") + @Throws(ServletRequestBindingException::class) + fun importFile(request: HttpServletRequest): ResponseEntity<*> { + var workbook: Workbook? = null + + try { + val multipartFile = (request as MultipartHttpServletRequest).getFile("multipartFileList") + workbook = XSSFWorkbook(multipartFile?.inputStream) + } catch (e: Exception) { + println("Excel Wrong") + println(e) + } + + return ResponseEntity.ok(projectsService.importFile(workbook)) + } } \ No newline at end of file diff --git a/src/main/java/com/ffii/tsms/modules/project/web/models/NewProjectRequest.kt b/src/main/java/com/ffii/tsms/modules/project/web/models/NewProjectRequest.kt index 1f59b33..aab4473 100644 --- a/src/main/java/com/ffii/tsms/modules/project/web/models/NewProjectRequest.kt +++ b/src/main/java/com/ffii/tsms/modules/project/web/models/NewProjectRequest.kt @@ -7,7 +7,7 @@ import java.time.LocalDate data class NewProjectRequest( // Project details // @field:NotBlank(message = "project code cannot be empty") -// val projectCode: String, + val projectCode: String?, @field:NotBlank(message = "project name cannot be empty") val projectName: String, val projectCategoryId: Long, @@ -16,8 +16,10 @@ data class NewProjectRequest( val projectId: Long?, val projectActualStart: LocalDate?, val projectActualEnd: LocalDate?, + val projectPlanEnd: LocalDate?, val isClpProject: Boolean?, val mainProjectId: Long?, + val projectStatus: String?, val serviceTypeId: Long, val fundingTypeId: Long, @@ -30,7 +32,7 @@ data class NewProjectRequest( // Client details val clientId: Long, - val clientContactId: Long, + val clientContactId: Long?, val clientSubsidiaryId: Long?, // Allocation diff --git a/src/main/resources/db/changelog/changes/20240617_01_cyril/01_update_task.sql b/src/main/resources/db/changelog/changes/20240617_01_cyril/01_update_task.sql new file mode 100644 index 0000000..5c6920d --- /dev/null +++ b/src/main/resources/db/changelog/changes/20240617_01_cyril/01_update_task.sql @@ -0,0 +1,9 @@ +-- liquibase formatted sql +-- changeset cyril:task + +INSERT +INTO + task +(name, taskGroupId) +VALUES + ('5.8 Manhour Import', 5); \ No newline at end of file diff --git a/src/main/resources/db/changelog/changes/20240617_01_cyril/02_update_project.sql b/src/main/resources/db/changelog/changes/20240617_01_cyril/02_update_project.sql new file mode 100644 index 0000000..880b7c1 --- /dev/null +++ b/src/main/resources/db/changelog/changes/20240617_01_cyril/02_update_project.sql @@ -0,0 +1,5 @@ +-- liquibase formatted sql +-- changeset cyril:task + +ALTER TABLE `project` + CHANGE COLUMN `name` `name` VARCHAR(255) CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_unicode_ci' NOT NULL ; \ No newline at end of file