# Conflicts: # src/main/java/com/ffii/tsms/modules/report/service/ReportService.kt # src/main/java/com/ffii/tsms/modules/report/web/ReportController.kttags/Baseline_30082024_BACKEND_UAT
| @@ -17,7 +17,7 @@ class CustomerContactService( | |||
| } | |||
| fun findByContactId(contactId: Long): CustomerContact { | |||
| return customerContactRepository.findById(contactId).orElseThrow() | |||
| return customerContactRepository.findById(contactId).orElse(null) | |||
| } | |||
| fun saveContactsByCustomer(saveCustomerId: Long, saveCustomerContact: List<CustomerContact>) { | |||
| @@ -178,7 +178,7 @@ open class StaffsService( | |||
| val company = companyRepository.findById(req.companyId).orElseThrow() | |||
| val grade = if (req.gradeId != null && req.gradeId > 0L) gradeRepository.findById(req.gradeId).orElseThrow() else null | |||
| val team = if (req.teamId != null && req.teamId > 0L) teamRepository.findById(req.teamId).orElseThrow() else null | |||
| val salary = salaryRepository.findById(req.salaryId).orElseThrow() | |||
| val salary = salaryRepository.findBySalaryPoint(req.salaryId).orElseThrow() | |||
| val department = departmentRepository.findById(req.departmentId).orElseThrow() | |||
| staff.apply { | |||
| @@ -123,4 +123,12 @@ open class Project : BaseEntity<Long>() { | |||
| // @JsonBackReference | |||
| @JoinColumn(name = "mainProjectId") | |||
| open var mainProject: Project? = null | |||
| @ManyToOne | |||
| @JoinColumn(name = "customerContactId") | |||
| open var customerContact: CustomerContact? = null | |||
| @ManyToOne | |||
| @JoinColumn(name = "subsidiaryContactId") | |||
| open var subsidiaryContact: SubsidiaryContact? = null | |||
| } | |||
| @@ -21,12 +21,12 @@ interface ProjectRepository : AbstractRepository<Project, Long> { | |||
| fun findAllByPlanStartLessThanEqualAndPlanEndGreaterThanEqual(remainedDateFrom: LocalDate?, remainedDateTo: LocalDate?):List<Project> | |||
| //fun findAllByDateRange(start: LocalDate, end: LocalDate): List<Project> | |||
| @Query("SELECT max(cast(substring_index(substring_index(p.code, '-', 2), '-', -1) as long)) FROM Project p WHERE p.isClpProject = ?1 and p.id != ?2" + | |||
| "") | |||
| fun getLatestCodeNumberByMainProject(isClpProject: Boolean, id: Serializable?): Long? | |||
| @Query("SELECT max(cast(substring_index(p.code, '-', -1) as long)) FROM Project p WHERE p.code like ?1 and p.id != ?2") | |||
| @Query("SELECT max(case when length(p.code) - length(replace(p.code, '-', '')) > 1 then cast(substring_index(p.code, '-', -1) as long) end) FROM Project p WHERE p.code like ?1 and p.id != ?2") | |||
| fun getLatestCodeNumberBySubProject(code: String, id: Serializable?): Long? | |||
| fun findAllByStatusIsNotAndMainProjectIsNull(status: String): List<Project> | |||
| } | |||
| @@ -0,0 +1,15 @@ | |||
| package com.ffii.tsms.modules.project.entity.projections | |||
| data class ProjectResourceReport ( | |||
| val code: String, | |||
| val name: String, | |||
| val team: String, | |||
| val client: String, | |||
| val plannedBudget: Double, | |||
| val actualConsumedBudget: Double, | |||
| val plannedManhour: Double, | |||
| val actualConsumedManhour: Double, | |||
| val budgetConsumptionRate: Double, | |||
| val manhourConsumptionRate: Double, | |||
| val status: String | |||
| ) | |||
| @@ -11,6 +11,9 @@ interface ProjectSearchInfo { | |||
| val code: String? | |||
| val status: String? | |||
| @get:Value("#{target.mainProject?.code}") | |||
| val mainProject: String? | |||
| @get:Value("#{target.projectCategory.name}") | |||
| val category: String? | |||
| @@ -6,6 +6,7 @@ import com.ffii.tsms.modules.data.service.CustomerContactService | |||
| import com.ffii.tsms.modules.project.entity.projections.ProjectSearchInfo | |||
| import com.ffii.tsms.modules.data.service.CustomerService | |||
| import com.ffii.tsms.modules.data.service.GradeService | |||
| import com.ffii.tsms.modules.data.service.SubsidiaryContactService | |||
| import com.ffii.tsms.modules.project.entity.* | |||
| import com.ffii.tsms.modules.project.entity.Milestone | |||
| import com.ffii.tsms.modules.project.entity.projections.InvoiceInfoSearchInfo | |||
| @@ -44,10 +45,12 @@ open class ProjectsService( | |||
| private val taskGroupRepository: TaskGroupRepository, | |||
| private val timesheetRepository: TimesheetRepository, | |||
| private val taskTemplateRepository: TaskTemplateRepository, | |||
| private val subsidiaryRepository: SubsidiaryRepository, private val subsidiaryContactRepository: SubsidiaryContactRepository | |||
| private val subsidiaryContactService: SubsidiaryContactService, | |||
| private val subsidiaryContactRepository: SubsidiaryContactRepository | |||
| ) { | |||
| open fun allProjects(): List<ProjectSearchInfo> { | |||
| return projectRepository.findProjectSearchInfoByOrderByCreatedDesc().sortedByDescending { it.status?.lowercase() != "deleted" } | |||
| return projectRepository.findProjectSearchInfoByOrderByCreatedDesc() | |||
| .sortedByDescending { it.status?.lowercase() != "deleted" } | |||
| } | |||
| open fun allInvoices(): List<InvoiceSearchInfo> { | |||
| @@ -63,63 +66,57 @@ open class ProjectsService( | |||
| } | |||
| open fun markDeleted(id: Long) { | |||
| projectRepository.save(projectRepository.findById(id).orElseThrow() | |||
| .apply { | |||
| deleted = true | |||
| status = "Deleted" | |||
| }) | |||
| projectRepository.save(projectRepository.findById(id).orElseThrow().apply { | |||
| deleted = true | |||
| status = "Deleted" | |||
| }) | |||
| } | |||
| open fun allAssignedProjects(): List<AssignedProject> { | |||
| return SecurityUtils.getUser().getOrNull()?.let { user -> | |||
| staffRepository.findByUserId(user.id).getOrNull()?.let { staff -> | |||
| staffAllocationRepository.findAssignedProjectsByStaff(staff) | |||
| .mapNotNull { | |||
| it.project?.let { project -> | |||
| val timesheetHours = timesheetRepository.totalHoursConsumedByProject(project) | |||
| AssignedProject( | |||
| id = project.id!!, | |||
| code = project.code!!, | |||
| name = project.name!!, | |||
| tasks = projectTaskRepository.findAllByProject(project).mapNotNull { pt -> pt.task }, | |||
| milestones = milestoneRepository.findAllByProject(project) | |||
| .filter { milestone -> milestone.taskGroup?.id != null } | |||
| .associateBy { milestone -> milestone.taskGroup!!.id!! } | |||
| .mapValues { (_, milestone) -> | |||
| MilestoneInfo( | |||
| startDate = milestone.startDate?.format(DateTimeFormatter.ISO_LOCAL_DATE), | |||
| endDate = milestone.endDate?.format(DateTimeFormatter.ISO_LOCAL_DATE) | |||
| ) | |||
| }, | |||
| hoursAllocated = project.totalManhour ?: 0.0, | |||
| hoursAllocatedOther = 0.0, | |||
| hoursSpent = timesheetHours.normalConsumed, | |||
| hoursSpentOther = timesheetHours.otConsumed | |||
| ) | |||
| } | |||
| staffAllocationRepository.findAssignedProjectsByStaff(staff).mapNotNull { | |||
| it.project?.let { project -> | |||
| val timesheetHours = timesheetRepository.totalHoursConsumedByProject(project) | |||
| AssignedProject(id = project.id!!, | |||
| code = project.code!!, | |||
| name = project.name!!, | |||
| tasks = projectTaskRepository.findAllByProject(project).mapNotNull { pt -> pt.task }, | |||
| milestones = milestoneRepository.findAllByProject(project) | |||
| .filter { milestone -> milestone.taskGroup?.id != null } | |||
| .associateBy { milestone -> milestone.taskGroup!!.id!! } | |||
| .mapValues { (_, milestone) -> | |||
| MilestoneInfo( | |||
| startDate = milestone.startDate?.format(DateTimeFormatter.ISO_LOCAL_DATE), | |||
| endDate = milestone.endDate?.format(DateTimeFormatter.ISO_LOCAL_DATE) | |||
| ) | |||
| }, | |||
| hoursAllocated = project.totalManhour ?: 0.0, | |||
| hoursAllocatedOther = 0.0, | |||
| hoursSpent = timesheetHours.normalConsumed, | |||
| hoursSpentOther = timesheetHours.otConsumed | |||
| ) | |||
| } | |||
| } | |||
| } | |||
| } ?: emptyList() | |||
| } | |||
| open fun allProjectWithTasks(): List<ProjectWithTasks> { | |||
| return projectRepository.findAll().map { project -> | |||
| ProjectWithTasks( | |||
| id = project.id!!, | |||
| ProjectWithTasks(id = project.id!!, | |||
| code = project.code!!, | |||
| name = project.name!!, | |||
| tasks = projectTaskRepository.findAllByProject(project).mapNotNull { pt -> pt.task }, | |||
| milestones = milestoneRepository.findAllByProject(project) | |||
| .filter { milestone -> milestone.taskGroup?.id != null } | |||
| .associateBy { milestone -> milestone.taskGroup!!.id!! } | |||
| .mapValues { (_, milestone) -> | |||
| .associateBy { milestone -> milestone.taskGroup!!.id!! }.mapValues { (_, milestone) -> | |||
| MilestoneInfo( | |||
| startDate = milestone.startDate?.format(DateTimeFormatter.ISO_LOCAL_DATE), | |||
| endDate = milestone.endDate?.format(DateTimeFormatter.ISO_LOCAL_DATE) | |||
| ) | |||
| } | |||
| ) | |||
| }) | |||
| } | |||
| } | |||
| @@ -135,15 +132,28 @@ open class ProjectsService( | |||
| if (latestProjectCode != null) { | |||
| val lastFix = latestProjectCode | |||
| return "$prefix-" + String.format("%04d", lastFix + 1L) | |||
| return "$prefix-" + String.format("%04d", lastFix + 1L) | |||
| } else { | |||
| return "$prefix-0001" | |||
| } | |||
| } | |||
| open fun createSubProjectCode(mainProject: Project, project: Project): String { | |||
| val prefix = mainProject.code | |||
| val latestProjectCode = projectRepository.getLatestCodeNumberBySubProject(prefix!!, project.id ?: -1) | |||
| if (latestProjectCode != null) { | |||
| val lastFix = latestProjectCode | |||
| return "$prefix-" + String.format("%03d", lastFix + 1L) | |||
| } else { | |||
| return "$prefix-001" | |||
| } | |||
| } | |||
| @Transactional | |||
| open fun saveProject(request: NewProjectRequest): NewProjectResponse { | |||
| val projectCategory = | |||
| projectCategoryRepository.findById(request.projectCategoryId).orElseThrow() | |||
| val projectCategory = projectCategoryRepository.findById(request.projectCategoryId).orElseThrow() | |||
| val fundingType = fundingTypeRepository.findById(request.fundingTypeId).orElseThrow() | |||
| val serviceType = serviceTypeRepository.findById(request.serviceTypeId).orElseThrow() | |||
| val contractType = contractTypeRepository.findById(request.contractTypeId).orElseThrow() | |||
| @@ -158,6 +168,7 @@ open class ProjectsService( | |||
| val subsidiaryContact = subsidiaryContactRepository.findById(request.clientContactId).orElse(null) | |||
| val clientContact = customerContactService.findByContactId(request.clientContactId) | |||
| val customerSubsidiary = request.clientSubsidiaryId?.let { subsidiaryService.findSubsidiary(it) } | |||
| val mainProject = if (request.mainProjectId != null && request.mainProjectId > 0) projectRepository.findById(request.mainProjectId).orElse(null) else null | |||
| val allTasksMap = tasksService.allTasks().associateBy { it.id } | |||
| val taskGroupMap = tasksService.allTaskGroups().associateBy { it.id } | |||
| @@ -169,7 +180,7 @@ open class ProjectsService( | |||
| project.apply { | |||
| name = request.projectName | |||
| description = request.projectDescription | |||
| code = if (this.code.isNullOrEmpty()) createProjectCode(request.isClpProject, project) else this.code | |||
| code = if (this.code.isNullOrEmpty() && request.mainProjectId == null) createProjectCode(request.isClpProject, project) else if (this.code.isNullOrEmpty() && request.mainProjectId != null && mainProject != null) createSubProjectCode(mainProject, project) else this.code | |||
| expectedTotalFee = request.expectedProjectFee | |||
| totalManhour = request.totalManhour | |||
| actualStart = request.projectActualStart | |||
| @@ -179,6 +190,7 @@ open class ProjectsService( | |||
| else if (this.actualStart != null) "On-going" | |||
| else "Pending To Start" | |||
| isClpProject = request.isClpProject | |||
| this.mainProject = mainProject | |||
| this.projectCategory = projectCategory | |||
| this.fundingType = fundingType | |||
| @@ -191,10 +203,15 @@ open class ProjectsService( | |||
| this.teamLead = teamLead | |||
| this.customer = customer | |||
| custLeadName = if (request.isSubsidiaryContact == null || !request.isSubsidiaryContact) clientContact.name else subsidiaryContact.name | |||
| custLeadEmail = if (request.isSubsidiaryContact == null || !request.isSubsidiaryContact) clientContact.email else subsidiaryContact.email | |||
| custLeadPhone = if (request.isSubsidiaryContact == null || !request.isSubsidiaryContact) clientContact.phone else subsidiaryContact.phone | |||
| custLeadName = | |||
| if (request.isSubsidiaryContact == null || !request.isSubsidiaryContact) clientContact.name else subsidiaryContact.name | |||
| custLeadEmail = | |||
| if (request.isSubsidiaryContact == null || !request.isSubsidiaryContact) clientContact.email else subsidiaryContact.email | |||
| custLeadPhone = | |||
| if (request.isSubsidiaryContact == null || !request.isSubsidiaryContact) clientContact.phone else subsidiaryContact.phone | |||
| this.customerSubsidiary = customerSubsidiary | |||
| this.customerContact = if (customerSubsidiary == null) clientContact else null | |||
| this.subsidiaryContact = if (customerSubsidiary != null) subsidiaryContact else null | |||
| } | |||
| @@ -203,8 +220,7 @@ open class ProjectsService( | |||
| val milestones = request.taskGroups.entries.map { (taskStageId, taskGroupAllocation) -> | |||
| val taskGroup = taskGroupRepository.findById(taskStageId).orElse(TaskGroup()) ?: TaskGroup() | |||
| val milestone = if (project.id != null && project.id!! > 0L) milestoneRepository.findByProjectAndTaskGroup( | |||
| project, | |||
| taskGroup | |||
| project, taskGroup | |||
| ) ?: Milestone() else Milestone() | |||
| milestone.apply { | |||
| val newMilestone = this | |||
| @@ -233,8 +249,7 @@ open class ProjectsService( | |||
| taskGroupAllocation.taskIds.map { taskId -> | |||
| val projectTask = | |||
| if (project.id != null && project.id!! > 0L) projectTaskRepository.findByProjectAndTask( | |||
| project, | |||
| allTasksMap[taskId]!! | |||
| project, allTasksMap[taskId]!! | |||
| ) ?: ProjectTask() else ProjectTask() | |||
| projectTask.apply { | |||
| @@ -252,8 +267,7 @@ open class ProjectsService( | |||
| val gradeAllocations = request.manhourPercentageByGrade.entries.map { | |||
| val gradeAllocation = | |||
| if (project.id != null && project.id!! > 0L) gradeAllocationRepository.findByProjectAndGrade( | |||
| project, | |||
| gradeMap[it.key]!! | |||
| project, gradeMap[it.key]!! | |||
| ) ?: GradeAllocation() else GradeAllocation() | |||
| gradeAllocation.apply { | |||
| @@ -313,16 +327,16 @@ open class ProjectsService( | |||
| val project = projectRepository.findById(projectId) | |||
| return project.getOrNull()?.let { | |||
| val customerContact = | |||
| it.customer?.id?.let { customerId -> customerContactService.findAllByCustomerId(customerId) } | |||
| ?: emptyList() | |||
| // val subsidiaryContact = it.customerSubsidiary?.id?.let { subsidiaryId -> | |||
| // subsidiaryContactService.findAllBySubsidiaryId(subsidiaryId) | |||
| // } ?: emptyList() | |||
| // val customerContact = it.customer?.id?.let { customerId -> customerContactService.findAllByCustomerId(customerId) } | |||
| // ?: emptyList() | |||
| val milestoneMap = it.milestones | |||
| .filter { milestone -> milestone.taskGroup?.id != null } | |||
| val milestoneMap = it.milestones.filter { milestone -> milestone.taskGroup?.id != null } | |||
| .associateBy { milestone -> milestone.taskGroup!!.id!! } | |||
| EditProjectDetails( | |||
| projectId = it.id, | |||
| EditProjectDetails(projectId = it.id, | |||
| projectDeleted = it.deleted, | |||
| projectCode = it.code, | |||
| projectName = it.name, | |||
| @@ -333,6 +347,7 @@ open class ProjectsService( | |||
| projectActualEnd = it.actualEnd, | |||
| projectStatus = it.status, | |||
| isClpProject = it.isClpProject, | |||
| mainProjectId = it.mainProject?.id, | |||
| serviceTypeId = it.serviceType?.id, | |||
| fundingTypeId = it.fundingType?.id, | |||
| contractTypeId = it.contractType?.id, | |||
| @@ -341,7 +356,8 @@ open class ProjectsService( | |||
| buildingTypeIds = it.buildingTypes.mapNotNull { buildingType -> buildingType.id }, | |||
| workNatureIds = it.workNatures.mapNotNull { workNature -> workNature.id }, | |||
| clientId = it.customer?.id, | |||
| clientContactId = customerContact.find { contact -> contact.name == it.custLeadName }?.id, | |||
| clientContactId = it.subsidiaryContact?.id ?: it.customerContact?.id, | |||
| // subsidiaryContact.find { contact -> contact.name == it.custLeadName }?.id ?: customerContact.find { contact -> contact.name == it.custLeadName }?.id, | |||
| clientSubsidiaryId = it.customerSubsidiary?.id, | |||
| totalManhour = it.totalManhour, | |||
| manhourPercentageByGrade = gradeAllocationRepository.findByProject(it) | |||
| @@ -349,8 +365,7 @@ open class ProjectsService( | |||
| .associate { allocation -> Pair(allocation.grade!!.id!!, allocation.manhour ?: 0.0) }, | |||
| taskGroups = projectTaskRepository.findAllByProject(it) | |||
| .mapNotNull { projectTask -> if (projectTask.task?.taskGroup?.id != null) projectTask.task else null } | |||
| .groupBy { task -> task.taskGroup!!.id!! } | |||
| .mapValues { (taskGroupId, tasks) -> | |||
| .groupBy { task -> task.taskGroup!!.id!! }.mapValues { (taskGroupId, tasks) -> | |||
| TaskGroupAllocation( | |||
| taskIds = tasks.mapNotNull { task -> task.id }, | |||
| percentAllocation = milestoneMap[taskGroupId]?.stagePercentAllocation ?: 0.0 | |||
| @@ -359,8 +374,9 @@ open class ProjectsService( | |||
| allocatedStaffIds = staffAllocationRepository.findByProject(it) | |||
| .mapNotNull { allocation -> allocation.staff?.id }, | |||
| milestones = milestoneMap.mapValues { (_, milestone) -> | |||
| com.ffii.tsms.modules.project.web.models.Milestone( | |||
| startDate = milestone.startDate?.format(DateTimeFormatter.ISO_LOCAL_DATE), | |||
| com.ffii.tsms.modules.project.web.models.Milestone(startDate = milestone.startDate?.format( | |||
| DateTimeFormatter.ISO_LOCAL_DATE | |||
| ), | |||
| endDate = milestone.endDate?.format(DateTimeFormatter.ISO_LOCAL_DATE), | |||
| payments = milestone.milestonePayments.map { payment -> | |||
| PaymentInputs( | |||
| @@ -369,8 +385,7 @@ open class ProjectsService( | |||
| description = payment.description!!, | |||
| date = payment.date!!.format(DateTimeFormatter.ISO_LOCAL_DATE) | |||
| ) | |||
| } | |||
| ) | |||
| }) | |||
| }, | |||
| expectedProjectFee = it.expectedTotalFee | |||
| ) | |||
| @@ -400,4 +415,37 @@ open class ProjectsService( | |||
| open fun allWorkNatures(): List<WorkNature> { | |||
| return workNatureRepository.findAll() | |||
| } | |||
| open fun allMainProjects(): List<MainProjectDetails> { | |||
| val mainProjects: List<Project> = projectRepository.findAllByStatusIsNotAndMainProjectIsNull("Deleted") | |||
| return mainProjects.map { project: Project -> | |||
| val subsidiaryContact = project.customerSubsidiary?.id?.let { subsidiaryId -> | |||
| subsidiaryContactService.findAllBySubsidiaryId(subsidiaryId) | |||
| } ?: emptyList() | |||
| val customerContact = project.customer?.id?.let { customerId -> customerContactService.findAllByCustomerId(customerId) } | |||
| ?: emptyList() | |||
| MainProjectDetails( | |||
| projectId = project.id, | |||
| projectCode = project.code, | |||
| projectName = project.name, | |||
| projectCategoryId = project.projectCategory?.id, | |||
| projectDescription = project.description, | |||
| projectLeadId = project.teamLead?.id, | |||
| projectStatus = project.status, | |||
| isClpProject = project.isClpProject, | |||
| serviceTypeId = project.serviceType?.id, | |||
| fundingTypeId = project.fundingType?.id, | |||
| contractTypeId = project.contractType?.id, | |||
| locationId = project.location?.id, | |||
| buildingTypeIds = project.buildingTypes.mapNotNull { buildingType -> buildingType.id }, | |||
| workNatureIds = project.workNatures.mapNotNull { workNature -> workNature.id }, | |||
| clientId = project.customer?.id, | |||
| clientContactId = subsidiaryContact.find { contact -> contact.name == project.custLeadName }?.id ?: customerContact.find { contact -> contact.name == project.custLeadName }?.id, | |||
| clientSubsidiaryId = project.customerSubsidiary?.id, | |||
| expectedProjectFee = project.expectedTotalFee | |||
| ) | |||
| } | |||
| } | |||
| } | |||
| @@ -2,8 +2,10 @@ package com.ffii.tsms.modules.project.web | |||
| import com.ffii.core.exception.NotFoundException | |||
| import com.ffii.tsms.modules.data.entity.* | |||
| import com.ffii.tsms.modules.project.entity.Project | |||
| import com.ffii.tsms.modules.project.entity.projections.ProjectSearchInfo | |||
| import com.ffii.tsms.modules.project.entity.ProjectCategory | |||
| import com.ffii.tsms.modules.project.entity.ProjectRepository | |||
| import com.ffii.tsms.modules.project.service.ProjectsService | |||
| import com.ffii.tsms.modules.project.web.models.* | |||
| import jakarta.validation.Valid | |||
| @@ -12,12 +14,17 @@ import org.springframework.web.bind.annotation.* | |||
| @RestController | |||
| @RequestMapping("/projects") | |||
| class ProjectsController(private val projectsService: ProjectsService) { | |||
| class ProjectsController(private val projectsService: ProjectsService, private val projectRepository: ProjectRepository) { | |||
| @GetMapping | |||
| fun allProjects(): List<ProjectSearchInfo> { | |||
| return projectsService.allProjects() | |||
| } | |||
| @GetMapping("/main") | |||
| fun allMainProjects(): List<MainProjectDetails> { | |||
| return projectsService.allMainProjects() | |||
| } | |||
| @DeleteMapping("/{id}") | |||
| @ResponseStatus(HttpStatus.NO_CONTENT) | |||
| fun deleteProject(@PathVariable id: Long) { | |||
| @@ -15,6 +15,7 @@ data class EditProjectDetails( | |||
| val projectActualEnd: LocalDate?, | |||
| val projectStatus: String?, | |||
| val isClpProject: Boolean?, | |||
| val mainProjectId: Long?, | |||
| val serviceTypeId: Long?, | |||
| val fundingTypeId: Long?, | |||
| @@ -0,0 +1,30 @@ | |||
| package com.ffii.tsms.modules.project.web.models | |||
| data class MainProjectDetails ( | |||
| // Project details | |||
| val projectId: Long?, | |||
| val projectCode: String?, | |||
| val projectName: String?, | |||
| val projectCategoryId: Long?, | |||
| val projectDescription: String?, | |||
| val projectLeadId: Long?, | |||
| val projectStatus: String?, | |||
| val isClpProject: Boolean?, | |||
| val serviceTypeId: Long?, | |||
| val fundingTypeId: Long?, | |||
| val contractTypeId: Long?, | |||
| val locationId: Long?, | |||
| val buildingTypeIds: List<Long>, | |||
| val workNatureIds: List<Long>, | |||
| // Client details | |||
| val clientId: Long?, | |||
| val clientContactId: Long?, | |||
| val clientSubsidiaryId: Long?, | |||
| val expectedProjectFee: Double?, | |||
| ) | |||
| @@ -17,6 +17,7 @@ data class NewProjectRequest( | |||
| val projectActualStart: LocalDate?, | |||
| val projectActualEnd: LocalDate?, | |||
| val isClpProject: Boolean?, | |||
| val mainProjectId: Long?, | |||
| val serviceTypeId: Long, | |||
| val fundingTypeId: Long, | |||
| @@ -7,7 +7,12 @@ import com.ffii.tsms.modules.data.entity.Staff | |||
| import com.ffii.tsms.modules.data.entity.Team | |||
| import com.ffii.tsms.modules.project.entity.Invoice | |||
| import com.ffii.tsms.modules.project.entity.Project | |||
| import com.ffii.tsms.modules.timesheet.entity.Leave | |||
| import com.ffii.tsms.modules.timesheet.entity.Timesheet | |||
| import com.ffii.tsms.modules.timesheet.entity.projections.MonthlyLeave | |||
| import com.ffii.tsms.modules.timesheet.entity.projections.ProjectMonthlyHoursWithDate | |||
| import com.ffii.tsms.modules.timesheet.entity.projections.TimesheetHours | |||
| import com.ffii.tsms.modules.timesheet.web.models.LeaveEntry | |||
| import org.apache.commons.logging.Log | |||
| import org.apache.commons.logging.LogFactory | |||
| import org.apache.poi.ss.usermodel.* | |||
| @@ -20,6 +25,7 @@ import org.springframework.stereotype.Service | |||
| import java.io.ByteArrayOutputStream | |||
| import java.io.IOException | |||
| import java.math.BigDecimal | |||
| import java.sql.Time | |||
| import java.time.LocalDate | |||
| import java.time.YearMonth | |||
| import java.time.format.DateTimeFormatter | |||
| @@ -27,6 +33,8 @@ import java.util.* | |||
| data class DayInfo(val date: String?, val weekday: String?) | |||
| @Service | |||
| open class ReportService( | |||
| private val jdbcDao: JdbcDao, | |||
| @@ -41,6 +49,7 @@ open class ReportService( | |||
| private val MONTHLY_WORK_HOURS_ANALYSIS_REPORT = "templates/report/AR08_Monthly Work Hours Analysis Report.xlsx" | |||
| private val SALART_LIST_TEMPLATE = "templates/report/Salary Template.xlsx" | |||
| private val LATE_START_REPORT = "templates/report/AR01_Late Start Report v01.xlsx" | |||
| private val RESOURCE_OVERCONSUMPTION_REPORT = "templates/report/AR03_Resource Overconsumption.xlsx" | |||
| // ==============================|| GENERATE REPORT ||============================== // | |||
| @@ -143,6 +152,29 @@ open class ReportService( | |||
| return outputStream.toByteArray() | |||
| } | |||
| @Throws(IOException::class) | |||
| fun generateProjectResourceOverconsumptionReport( | |||
| team: String, | |||
| customer: String, | |||
| result: List<Map<String, Any>>, | |||
| lowerLimit: Double | |||
| ): ByteArray { | |||
| // Generate the Excel report with query results | |||
| val workbook: Workbook = createProjectResourceOverconsumptionReport( | |||
| team, | |||
| customer, | |||
| result, | |||
| lowerLimit, | |||
| RESOURCE_OVERCONSUMPTION_REPORT | |||
| ) | |||
| // Write the workbook to a ByteArrayOutputStream | |||
| val outputStream: ByteArrayOutputStream = ByteArrayOutputStream() | |||
| workbook.write(outputStream) | |||
| workbook.close() | |||
| return outputStream.toByteArray() | |||
| } | |||
| @Throws(IOException::class) | |||
| fun exportSalaryList(salarys: List<Salary>): ByteArray { | |||
| // Generate the Excel report with query results | |||
| @@ -733,10 +765,13 @@ open class ReportService( | |||
| var projectList: List<String> = listOf() | |||
| println("----timesheets-----") | |||
| println(timesheets) | |||
| println("----leaves-----") | |||
| println(leaves) | |||
| // result = timesheet record mapped | |||
| var result: Map<String, Any> = mapOf() | |||
| if (timesheets.isNotEmpty()) { | |||
| projectList = timesheets.map{ "${it["code"]}\n ${it["name"]}"}.toList() | |||
| projectList = timesheets.map{ "${it["code"]}\n ${it["name"]}"}.toList().distinct() | |||
| result = timesheets.groupBy( | |||
| { it["id"].toString() }, | |||
| { mapOf( | |||
| @@ -916,41 +951,40 @@ open class ReportService( | |||
| columnSize++ | |||
| } | |||
| println(columnSize) | |||
| ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// | |||
| /////////////////////////////////////////////////////////////// main data ////////////////////////////////////////////////////////////// | |||
| if (timesheets.isNotEmpty()) { | |||
| projectList.forEach { _ -> | |||
| projectList.forEachIndexed { index, _ -> | |||
| for (i in 0 until rowSize) { | |||
| tempCell = sheet.getRow(8 + i).createCell(columnIndex) | |||
| tempCell = sheet.getRow(8 + i).createCell(columnIndex + index) | |||
| tempCell.setCellValue(0.0) | |||
| tempCell.cellStyle.dataFormat = accountingStyle | |||
| } | |||
| result.forEach{(id, list) -> | |||
| for (i in 0 until id.toInt()) { | |||
| val temp: List<Map<String, Any>> = list as List<Map<String, Any>> | |||
| temp.forEachIndexed { i, _ -> | |||
| dayInt = temp[i]["date"].toString().toInt() | |||
| tempCell = sheet.getRow(dayInt.plus(7)).createCell(columnIndex) | |||
| tempCell.setCellValue(temp[i]["normalConsumed"] as Double) | |||
| } | |||
| } | |||
| result.forEach{ (id, list) -> | |||
| val temp: List<Map<String, Any>> = list as List<Map<String, Any>> | |||
| temp.forEachIndexed { index, _ -> | |||
| dayInt = temp[index]["date"].toString().toInt() | |||
| tempCell = sheet.getRow(dayInt.plus(7)).createCell(columnIndex) | |||
| tempCell.setCellValue((temp[index]["normalConsumed"] as Double) + (temp[index]["otConsumed"] as Double)) | |||
| } | |||
| } | |||
| columnIndex++ | |||
| } | |||
| } | |||
| // leave hours data | |||
| /////////////////////////////////////////////////////////////// leave hours data ////////////////////////////////////////////////////////////// | |||
| for (i in 0 until rowSize) { | |||
| tempCell = sheet.getRow(8 + i).createCell(columnIndex) | |||
| tempCell.setCellValue(0.0) | |||
| tempCell.cellStyle.dataFormat = accountingStyle | |||
| } | |||
| if (leaves.isNotEmpty()) { | |||
| leaves.forEach { leave -> | |||
| for (i in 0 until rowSize) { | |||
| tempCell = sheet.getRow(8 + i).createCell(columnIndex) | |||
| tempCell.setCellValue(0.0) | |||
| tempCell.cellStyle.dataFormat = accountingStyle | |||
| } | |||
| dayInt = leave["recordDate"].toString().toInt() | |||
| tempCell = sheet.getRow(dayInt.plus(7)).createCell(columnIndex) | |||
| tempCell.setCellValue(leave["leaveHours"] as Double) | |||
| } | |||
| } | |||
| ///////////////////////////////////////////////////////// Leave Hours //////////////////////////////////////////////////////////////////// | |||
| ///////////////////////////////////////////////////////// Leave Hours title //////////////////////////////////////////////////////////////////// | |||
| tempCell = sheet.getRow(rowIndex).createCell(columnIndex) | |||
| tempCell.setCellValue("Leave Hours") | |||
| tempCell.cellStyle = boldStyle | |||
| @@ -1027,6 +1061,73 @@ open class ReportService( | |||
| return workbook | |||
| } | |||
| private fun createProjectResourceOverconsumptionReport( | |||
| team: String, | |||
| customer: String, | |||
| result: List<Map<String, Any>>, | |||
| lowerLimit: Double, | |||
| templatePath: String | |||
| ):Workbook { | |||
| val resource = ClassPathResource(templatePath) | |||
| val templateInputStream = resource.inputStream | |||
| val workbook: Workbook = XSSFWorkbook(templateInputStream) | |||
| val accountingStyle = workbook.createDataFormat().getFormat("_(* #,##0.00_);_(* (#,##0.00);_(* \"-\"??_);_(@_)") | |||
| val sheet: Sheet = workbook.getSheetAt(0) | |||
| val alignCenterStyle = workbook.createCellStyle() | |||
| alignCenterStyle.alignment = HorizontalAlignment.CENTER // Set the alignment to center | |||
| var rowIndex = 1 // Assuming the location is in (1,2), which is the report date field | |||
| var columnIndex = 2 | |||
| var tempRow = sheet.getRow(rowIndex) | |||
| var tempCell = tempRow.getCell(columnIndex) | |||
| tempCell.setCellValue(FORMATTED_TODAY) | |||
| rowIndex = 2 | |||
| tempCell = sheet.getRow(rowIndex).getCell(columnIndex) | |||
| tempCell.setCellValue(team) | |||
| rowIndex = 3 | |||
| tempCell = sheet.getRow(rowIndex).getCell(columnIndex) | |||
| tempCell.setCellValue(customer) | |||
| rowIndex = 6 | |||
| columnIndex = 0 | |||
| result.forEachIndexed { index, obj -> | |||
| tempCell = sheet.getRow(rowIndex).createCell(columnIndex) | |||
| tempCell.setCellValue((index + 1).toDouble()) | |||
| val keys = obj.keys.toList() | |||
| keys.forEachIndexed { keyIndex, key -> | |||
| tempCell = sheet.getRow(rowIndex).getCell(columnIndex + keyIndex + 1) ?: sheet.getRow(rowIndex).createCell(columnIndex + keyIndex + 1) | |||
| when (obj[key]) { | |||
| is Double -> tempCell.setCellValue(obj[key] as Double) | |||
| else -> tempCell.setCellValue(obj[key] as String ) | |||
| } | |||
| } | |||
| rowIndex++ | |||
| } | |||
| val sheetCF = sheet.sheetConditionalFormatting | |||
| val rule1 = sheetCF.createConditionalFormattingRule("AND(J7 >= $lowerLimit, J7 <= 1)") | |||
| val rule2 = sheetCF.createConditionalFormattingRule("J7 > 1") | |||
| var fillOrange = rule1.createPatternFormatting() | |||
| fillOrange.setFillBackgroundColor(IndexedColors.LIGHT_ORANGE.index); | |||
| fillOrange.setFillPattern(PatternFormatting.SOLID_FOREGROUND) | |||
| var fillRed = rule2.createPatternFormatting() | |||
| fillRed.setFillBackgroundColor(IndexedColors.ROSE.index); | |||
| fillRed.setFillPattern(PatternFormatting.SOLID_FOREGROUND) | |||
| val cfRules = arrayOf(rule1, rule2) | |||
| val regions = arrayOf(CellRangeAddress.valueOf("\$J7:\$K${rowIndex+1}")) | |||
| sheetCF.addConditionalFormatting(regions, cfRules); | |||
| return workbook | |||
| } | |||
| //createLateStartReport | |||
| private fun createLateStartReport( | |||
| team: Team, | |||
| @@ -1173,6 +1274,81 @@ open class ReportService( | |||
| return jdbcDao.queryForList(sql.toString(), args) | |||
| } | |||
| open fun getProjectResourceOverconsumptionReport(args: Map<String, Any>): List<Map<String, Any>> { | |||
| val sql = StringBuilder("WITH teamNormalConsumed AS (" | |||
| + " SELECT " | |||
| + " s.teamId, " | |||
| + " pt.project_id, " | |||
| + " SUM(tns.totalConsumed) AS totalConsumed " | |||
| + " FROM ( " | |||
| + " SELECT " | |||
| + " t.staffId, " | |||
| + " sum(t.normalConsumed + COALESCE(t.otConsumed, 0)) as totalConsumed, " | |||
| + " t.projectTaskId AS taskId " | |||
| + " FROM timesheet t " | |||
| + " LEFT JOIN staff s ON t.staffId = s.id " | |||
| + " GROUP BY t.staffId, t.projectTaskId " | |||
| + " ) AS tns " | |||
| + " INNER JOIN project_task pt ON tns.taskId = pt.id " | |||
| + " JOIN staff s ON tns.staffId = s.id " | |||
| + " JOIN team t ON s.teamId = t.id " | |||
| + " GROUP BY teamId, project_id " | |||
| + " ) " | |||
| + " SELECT " | |||
| + " p.code, " | |||
| + " p.name, " | |||
| + " t.code as team, " | |||
| + " c.code as client, " | |||
| + " p.expectedTotalFee as plannedBudget, " | |||
| + " COALESCE((tns.totalConsumed * sa.hourlyRate), 0) as actualConsumedBudget, " | |||
| + " COALESCE(p.totalManhour, 0) as plannedManhour, " | |||
| + " COALESCE(tns.totalConsumed, 0) as actualConsumedManhour, " | |||
| + " (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) as budgetConsumptionRate, " | |||
| + " (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) as manhourConsumptionRate, " | |||
| + " CASE " | |||
| + " when (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= :lowerLimit and (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) <= 1 " | |||
| + " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= :lowerLimit and (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) <= 1 " | |||
| + " then 'Potential Overconsumption' " | |||
| + " when (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= 1 " | |||
| + " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= 1 " | |||
| + " then 'Overconsumption' " | |||
| + " else 'Within Budget' " | |||
| + " END as status " | |||
| + " FROM project p " | |||
| + " LEFT JOIN team t ON p.teamLead = t.teamLead " | |||
| + " LEFT JOIN staff s ON p.teamLead = s.id " | |||
| + " LEFT JOIN salary sa ON s.salaryId = sa.salaryPoint " | |||
| + " LEFT JOIN customer c ON p.customerId = c.id " | |||
| + " left join teamNormalConsumed tns on tns.project_id = p.id " | |||
| + " WHERE p.deleted = false " | |||
| + " and p.status = 'On-going' " | |||
| ) | |||
| if (args != null) { | |||
| var statusFilter: String = "" | |||
| if (args.containsKey("teamId")) | |||
| sql.append("and t.id = :teamId") | |||
| if (args.containsKey("custId")) | |||
| sql.append("and c.id = :custId") | |||
| if (args.containsKey("status")) | |||
| statusFilter = when (args.get("status")) { | |||
| "Potential Overconsumption" -> "and " + | |||
| " ((COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= 0.9 " + | |||
| " and (COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) <= 1 " + | |||
| " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= 0.9 " + | |||
| " and (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) <= 1)" | |||
| "Overconsumption" -> "and " + | |||
| " ((COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) >= 1 " + | |||
| " or (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) >= 1) " | |||
| "Within Budget" -> "and " + | |||
| " ((COALESCE((tns.totalConsumed * sa.hourlyRate), 0) / p.expectedTotalFee) < 0.9 " + | |||
| " and " + | |||
| " (COALESCE(tns.totalConsumed, 0) / COALESCE(p.totalManhour, 0)) <= 0.9 " | |||
| else -> "" | |||
| } | |||
| sql.append(statusFilter) | |||
| } | |||
| return jdbcDao.queryForList(sql.toString(), args) | |||
| } | |||
| open fun getManhoursSpent(projectId: Long, startMonth: String, endMonth: String): List<Map<String, Any>>{ | |||
| val sql = StringBuilder( | |||
| " with cte_timesheet as (" | |||
| @@ -1,11 +1,23 @@ | |||
| package com.ffii.tsms.modules.report.web | |||
| import com.fasterxml.jackson.databind.ObjectMapper | |||
| import com.fasterxml.jackson.module.kotlin.KotlinModule | |||
| import com.ffii.tsms.modules.data.entity.Customer | |||
| import com.ffii.tsms.modules.data.entity.StaffRepository | |||
| //import com.ffii.tsms.modules.data.entity.projections.FinancialStatusReportInfo | |||
| import com.ffii.tsms.modules.data.entity.projections.StaffSearchInfo | |||
| import com.ffii.tsms.modules.data.entity.Team | |||
| import com.ffii.tsms.modules.data.service.CustomerService | |||
| import com.ffii.tsms.modules.data.service.TeamService | |||
| import com.ffii.tsms.modules.project.entity.* | |||
| import com.ffii.tsms.modules.report.service.ReportService | |||
| import com.ffii.tsms.modules.project.entity.projections.ProjectResourceReport | |||
| import com.ffii.tsms.modules.project.service.InvoiceService | |||
| import com.ffii.tsms.modules.report.service.ReportService | |||
| import com.ffii.tsms.modules.report.web.model.FinancialStatusReportRequest | |||
| import com.ffii.tsms.modules.report.web.model.ProjectCashFlowReportRequest | |||
| import com.ffii.tsms.modules.report.web.model.ProjectResourceOverconsumptionReport | |||
| import com.ffii.tsms.modules.report.web.model.StaffMonthlyWorkHourAnalysisReportRequest | |||
| import com.ffii.tsms.modules.report.web.model.LateStartReportRequest | |||
| import com.ffii.tsms.modules.project.entity.ProjectRepository | |||
| import com.ffii.tsms.modules.timesheet.entity.LeaveRepository | |||
| import com.ffii.tsms.modules.timesheet.entity.TimesheetRepository | |||
| @@ -13,17 +25,11 @@ import com.ffii.tsms.modules.timesheet.entity.projections.ProjectMonthlyHoursWit | |||
| import jakarta.validation.Valid | |||
| import org.springframework.core.io.ByteArrayResource | |||
| import org.springframework.core.io.Resource | |||
| import org.springframework.http.ResponseEntity | |||
| import org.springframework.web.bind.ServletRequestBindingException | |||
| import org.springframework.web.bind.annotation.GetMapping | |||
| import org.springframework.web.bind.annotation.PathVariable | |||
| import org.springframework.web.bind.annotation.PostMapping | |||
| import org.springframework.web.bind.annotation.RequestBody | |||
| import org.springframework.web.bind.annotation.RequestMapping | |||
| import org.springframework.web.bind.annotation.RequestParam | |||
| import org.springframework.web.bind.annotation.RestController | |||
| import org.springframework.http.HttpHeaders | |||
| import org.springframework.http.MediaType | |||
| import org.springframework.http.ResponseEntity | |||
| import org.springframework.web.bind.ServletRequestBindingException | |||
| import org.springframework.web.bind.annotation.* | |||
| import java.io.IOException | |||
| import java.time.LocalDate | |||
| import java.net.URLEncoder | |||
| @@ -32,7 +38,6 @@ import org.springframework.stereotype.Controller | |||
| import com.ffii.tsms.modules.data.entity.TeamRepository | |||
| import com.ffii.tsms.modules.data.entity.CustomerRepository | |||
| import org.springframework.data.jpa.repository.JpaRepository | |||
| import com.ffii.tsms.modules.data.entity.Customer | |||
| import com.ffii.tsms.modules.project.entity.Project | |||
| import com.ffii.tsms.modules.report.web.model.* | |||
| @@ -49,6 +54,8 @@ class ReportController( | |||
| private val customerRepository: CustomerRepository, | |||
| private val staffRepository: StaffRepository, | |||
| private val leaveRepository: LeaveRepository, | |||
| private val teamService: TeamService, | |||
| private val customerService: CustomerService, | |||
| private val invoiceService: InvoiceService) { | |||
| @PostMapping("/fetchProjectsFinancialStatusReport") | |||
| @@ -87,13 +94,16 @@ class ReportController( | |||
| val nextMonth = request.yearMonth.plusMonths(1).atDay(1) | |||
| val staff = staffRepository.findById(request.id).orElseThrow() | |||
| println(thisMonth) | |||
| println(nextMonth) | |||
| val args: Map<String, Any> = mutableMapOf( | |||
| "staffId" to request.id, | |||
| "startDate" to thisMonth, | |||
| "endDate" to nextMonth, | |||
| ) | |||
| val timesheets= excelReportService.getTimesheet(args) | |||
| val leaves= excelReportService.getLeaves(args) | |||
| val timesheets = excelReportService.getTimesheet(args) | |||
| val leaves = excelReportService.getLeaves(args) | |||
| println(leaves) | |||
| val reportResult: ByteArray = excelReportService.generateStaffMonthlyWorkHourAnalysisReport(thisMonth, staff, timesheets, leaves) | |||
| // val mediaType: MediaType = MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") | |||
| @@ -102,6 +112,34 @@ class ReportController( | |||
| .header("filename", "Monthly Work Hours Analysis Report - " + staff.name + " - " + LocalDate.now() + ".xlsx") | |||
| .body(ByteArrayResource(reportResult)) | |||
| } | |||
| private val mapper = ObjectMapper().registerModule(KotlinModule()) | |||
| @PostMapping("/ProjectResourceOverconsumptionReport") | |||
| @Throws(ServletRequestBindingException::class, IOException::class) | |||
| fun ProjectResourceOverconsumptionReport(@RequestBody @Valid request: ProjectResourceOverconsumptionReport): ResponseEntity<Resource> { | |||
| val lowerLimit = request.lowerLimit | |||
| var team: String = "All" | |||
| var customer: String = "All" | |||
| val args: MutableMap<String, Any> = mutableMapOf( | |||
| "status" to request.status, | |||
| "lowerLimit" to lowerLimit | |||
| ) | |||
| if (request.teamId != null) { | |||
| args["teamId"] = request.teamId | |||
| team = teamService.find(request.teamId).orElseThrow().name | |||
| } | |||
| if (request.custId != null) { | |||
| args["custId"] = request.custId | |||
| customer = customerService.find(request.custId).orElseThrow().name | |||
| } | |||
| val result: List<Map<String, Any>> = excelReportService.getProjectResourceOverconsumptionReport(args); | |||
| val reportResult: ByteArray = excelReportService.generateProjectResourceOverconsumptionReport(team, customer, result, lowerLimit) | |||
| // val mediaType: MediaType = MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") | |||
| return ResponseEntity.ok() | |||
| // .contentType(mediaType) | |||
| .header("filename", "Project Resource Overconsumption Report - " + " - " + LocalDate.now() + ".xlsx") | |||
| .body(ByteArrayResource(reportResult)) | |||
| } | |||
| @GetMapping("/test/{id}") | |||
| fun test(@PathVariable id: Long): List<Invoice> { | |||
| @@ -30,4 +30,10 @@ data class LateStartReportRequest ( | |||
| val remainedDateTo: LocalDate, | |||
| val customer: String, | |||
| val reportDate: LocalDate | |||
| ) | |||
| data class ProjectResourceOverconsumptionReport ( | |||
| val teamId: Long?, | |||
| val custId: Long?, | |||
| val status: String, | |||
| val lowerLimit: Double | |||
| ) | |||
| @@ -0,0 +1,20 @@ | |||
| -- liquibase formatted sql | |||
| -- changeset cyril:project | |||
| ALTER TABLE `project` | |||
| ADD COLUMN `customerContactId` INT NULL DEFAULT NULL AFTER `mainProjectId`, | |||
| ADD COLUMN `subsidiaryContactId` INT NULL DEFAULT NULL AFTER `customerContactId`, | |||
| ADD INDEX `FK_PROJECT_ON_CUSTOMERCONTACTID` (`customerContactId` ASC) VISIBLE, | |||
| ADD INDEX `FK_PROJECT_ON_SUBSIDIARYCONTACTID` (`subsidiaryContactId` ASC) VISIBLE; | |||
| ; | |||
| ALTER TABLE `project` | |||
| ADD CONSTRAINT `FK_PROJECT_ON_CUSTOMERCONTACTID` | |||
| FOREIGN KEY (`customerContactId`) | |||
| REFERENCES `customer_contact` (`id`) | |||
| ON DELETE NO ACTION | |||
| ON UPDATE NO ACTION, | |||
| ADD CONSTRAINT `FK_PROJECT_ON_SUBSIDIARYCONTACTID` | |||
| FOREIGN KEY (`subsidiaryContactId`) | |||
| REFERENCES `subsidiary_contact` (`id`) | |||
| ON DELETE NO ACTION | |||
| ON UPDATE NO ACTION; | |||