@@ -1,10 +1,14 @@ | |||||
package com.ffii.tsms.modules.data.entity; | package com.ffii.tsms.modules.data.entity; | ||||
import com.fasterxml.jackson.annotation.JsonManagedReference; | |||||
import com.ffii.core.entity.BaseEntity; | import com.ffii.core.entity.BaseEntity; | ||||
import jakarta.persistence.*; | import jakarta.persistence.*; | ||||
import jakarta.validation.constraints.NotNull; | import jakarta.validation.constraints.NotNull; | ||||
import java.time.LocalTime; | import java.time.LocalTime; | ||||
import java.util.List; | |||||
import static jakarta.persistence.CascadeType.ALL; | |||||
@Entity | @Entity | ||||
@Table(name = "subsidiary") | @Table(name = "subsidiary") | ||||
@@ -31,6 +35,10 @@ public class Subsidiary extends BaseEntity<Long> { | |||||
@JoinColumn(name = "typeId") | @JoinColumn(name = "typeId") | ||||
private SubsidiaryType subsidiaryType; | private SubsidiaryType subsidiaryType; | ||||
@OneToMany(mappedBy = "subsidiary", cascade = ALL, orphanRemoval = true) | |||||
@JsonManagedReference | |||||
private List<SubsidiaryContact> subsidiaryContacts; | |||||
public String getAddress() { | public String getAddress() { | ||||
return address; | return address; | ||||
} | } | ||||
@@ -78,4 +86,12 @@ public class Subsidiary extends BaseEntity<Long> { | |||||
public void setSubsidiaryType(SubsidiaryType subsidiaryType) { | public void setSubsidiaryType(SubsidiaryType subsidiaryType) { | ||||
this.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; | 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 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; | import jakarta.validation.constraints.NotNull; | ||||
@Entity | @Entity | ||||
@Table(name = "subsidiary_contact") | @Table(name = "subsidiary_contact") | ||||
public class SubsidiaryContact extends IdEntity<Long> { | public class SubsidiaryContact extends IdEntity<Long> { | ||||
@NotNull | @NotNull | ||||
@OneToOne | |||||
@ManyToOne | |||||
@JsonBackReference | |||||
@JoinColumn(name = "subsidiaryId") | @JoinColumn(name = "subsidiaryId") | ||||
private Subsidiary subsidiary; | private Subsidiary subsidiary; | ||||
@@ -9,5 +9,7 @@ import java.util.Optional; | |||||
public interface SubsidiaryRepository extends AbstractRepository<Subsidiary, Long> { | public interface SubsidiaryRepository extends AbstractRepository<Subsidiary, Long> { | ||||
List<Subsidiary> findAllByDeletedFalse(); | List<Subsidiary> findAllByDeletedFalse(); | ||||
List<Subsidiary> findAllByDeletedFalseAndIdIn(List<Long> id); | |||||
Optional<Subsidiary> findByCode(@Param("code") String code); | Optional<Subsidiary> findByCode(@Param("code") String code); | ||||
} | } |
@@ -22,4 +22,7 @@ interface StaffSearchInfo { | |||||
@get:Value("#{target.currentPosition?.name}") | @get:Value("#{target.currentPosition?.name}") | ||||
val currentPosition: String? | val currentPosition: String? | ||||
@get:Value("#{target.user?.id}") | |||||
val userId: Long? | |||||
} | } |
@@ -1,5 +1,6 @@ | |||||
package com.ffii.tsms.modules.data.service | package com.ffii.tsms.modules.data.service | ||||
import com.ffii.core.exception.UnprocessableEntityException | |||||
import com.ffii.core.support.AbstractBaseEntityService | import com.ffii.core.support.AbstractBaseEntityService | ||||
import com.ffii.core.support.JdbcDao | import com.ffii.core.support.JdbcDao | ||||
import com.ffii.tsms.modules.common.SecurityUtils | 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.data.web.models.NewStaffRequest | ||||
import com.ffii.tsms.modules.user.entity.User | import com.ffii.tsms.modules.user.entity.User | ||||
import com.ffii.tsms.modules.user.entity.UserRepository | 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.security.crypto.password.PasswordEncoder | ||||
import org.springframework.stereotype.Service | import org.springframework.stereotype.Service | ||||
import org.springframework.transaction.annotation.Transactional | import org.springframework.transaction.annotation.Transactional | ||||
import java.util.* | import java.util.* | ||||
import java.util.stream.Collectors | |||||
import kotlin.jvm.optionals.getOrNull | import kotlin.jvm.optionals.getOrNull | ||||
@@ -96,6 +95,13 @@ open class StaffsService( | |||||
@Transactional(rollbackFor = [Exception::class]) | @Transactional(rollbackFor = [Exception::class]) | ||||
open fun saveStaff(req: NewStaffRequest): Staff { | 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 currentPosition = positionRepository.findById(req.currentPositionId).orElseThrow() | ||||
val joinPosition = positionRepository.findById(req.joinPositionId).orElseThrow() | val joinPosition = positionRepository.findById(req.joinPositionId).orElseThrow() | ||||
val company = companyRepository.findById(req.companyId).orElseThrow() | val company = companyRepository.findById(req.companyId).orElseThrow() | ||||
@@ -39,7 +39,7 @@ open class TeamService( | |||||
@Transactional(rollbackFor = [Exception::class]) | @Transactional(rollbackFor = [Exception::class]) | ||||
open fun saveTeam(req: NewTeamRequest): Team { | open fun saveTeam(req: NewTeamRequest): Team { | ||||
val ids = req.addStaffIds!! | val ids = req.addStaffIds!! | ||||
println(ids) | |||||
// println(ids) | |||||
val teamLead = staffRepository.findById(ids[0]).orElseThrow() | val teamLead = staffRepository.findById(ids[0]).orElseThrow() | ||||
val teamName = "Team " + teamLead.name | 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 com.ffii.tsms.modules.data.web.models.NewSkillRequest | ||||
import jakarta.servlet.http.HttpServletRequest | import jakarta.servlet.http.HttpServletRequest | ||||
import jakarta.validation.Valid | import jakarta.validation.Valid | ||||
import org.springframework.http.HttpStatus | |||||
import org.springframework.web.bind.ServletRequestBindingException | import org.springframework.web.bind.ServletRequestBindingException | ||||
import org.springframework.web.bind.annotation.* | import org.springframework.web.bind.annotation.* | ||||
@@ -25,6 +26,11 @@ class SkillController(private val skillService: SkillService) { | |||||
args["id"] = id | args["id"] = id | ||||
return skillService.list(args); | return skillService.list(args); | ||||
} | } | ||||
@DeleteMapping("/delete/{id}") | |||||
@ResponseStatus(HttpStatus.NO_CONTENT) | |||||
fun delete(@PathVariable id: Long?) { | |||||
skillService.markDelete(id) | |||||
} | |||||
@GetMapping | @GetMapping | ||||
fun list(): List<Map<String, Any>> { | fun list(): List<Map<String, Any>> { | ||||
@@ -5,10 +5,6 @@ import jakarta.validation.constraints.NotNull | |||||
import java.time.LocalDate | import java.time.LocalDate | ||||
data class NewStaffRequest( | 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") | @field:NotBlank(message = "Staff name cannot be empty") | ||||
val name: String, | val name: String, | ||||
@field:NotBlank(message = "Staff staffId cannot be empty") | @field:NotBlank(message = "Staff staffId cannot be empty") | ||||
@@ -17,21 +13,30 @@ data class NewStaffRequest( | |||||
val companyId: Long, | val companyId: Long, | ||||
@field:NotNull(message = "Staff salaryId cannot be empty") | @field:NotNull(message = "Staff salaryId cannot be empty") | ||||
val salaryId: Long, | 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, | val joinDate: LocalDate, | ||||
@field:NotNull(message = "Staff currentPositionId cannot be empty") | |||||
val currentPositionId: Long, | val currentPositionId: Long, | ||||
// val salaryEffId: Long, | |||||
@field:NotNull(message = "Staff joinPositionId cannot be empty") | |||||
val joinPositionId: Long, | val joinPositionId: Long, | ||||
val gradeId: Long?, | |||||
val teamId: Long?, | |||||
@field:NotNull(message = "Staff departmentId cannot be empty") | |||||
val departmentId: Long, | val departmentId: Long, | ||||
@field:NotBlank(message = "Staff phone1 cannot be empty") | |||||
val phone1: String, | val phone1: String, | ||||
val phone2: String?, | |||||
@field:NotBlank(message = "Staff email cannot be empty") | |||||
val email: String, | val email: String, | ||||
@field:NotBlank(message = "Staff emergContactName cannot be empty") | |||||
val emergContactName: String, | val emergContactName: String, | ||||
@field:NotBlank(message = "Staff emergContactPhone cannot be empty") | |||||
val emergContactPhone: String, | val emergContactPhone: String, | ||||
@field:NotBlank(message = "Staff employType cannot be empty") | |||||
val employType: String, | val employType: String, | ||||
val id: Long?, | |||||
val skillSetId: List<Long>?, | |||||
val gradeId: Long?, | |||||
val phone2: String?, | |||||
val teamId: Long?, | |||||
val departDate: LocalDate?, | val departDate: LocalDate?, | ||||
val departReason: String?, | val departReason: String?, | ||||
val remark: String?, | val remark: String?, |
@@ -108,4 +108,14 @@ open class Project : BaseEntity<Long>() { | |||||
inverseJoinColumns = [JoinColumn(name = "workNaturesId")] | inverseJoinColumns = [JoinColumn(name = "workNaturesId")] | ||||
) | ) | ||||
open var workNatures: MutableSet<WorkNature> = mutableSetOf() | 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.InvoiceInfoSearchInfo | ||||
import com.ffii.tsms.modules.project.entity.projections.InvoiceSearchInfo | import com.ffii.tsms.modules.project.entity.projections.InvoiceSearchInfo | ||||
import com.ffii.tsms.modules.project.entity.projections.ProjectSearchInfo | import com.ffii.tsms.modules.project.entity.projections.ProjectSearchInfo | ||||
import java.io.Serializable | |||||
interface ProjectRepository : AbstractRepository<Project, Long> { | interface ProjectRepository : AbstractRepository<Project, Long> { | ||||
fun findProjectSearchInfoByOrderByCreatedDesc(): List<ProjectSearchInfo> | fun findProjectSearchInfoByOrderByCreatedDesc(): List<ProjectSearchInfo> | ||||
@@ -13,4 +14,6 @@ interface ProjectRepository : AbstractRepository<Project, Long> { | |||||
fun findInvoiceSearchInfoById(id: Long): List<InvoiceSearchInfo> | fun findInvoiceSearchInfoById(id: Long): List<InvoiceSearchInfo> | ||||
fun findInvoiceInfoSearchInfoById(id: Long): List<InvoiceInfoSearchInfo> | fun findInvoiceInfoSearchInfoById(id: Long): List<InvoiceInfoSearchInfo> | ||||
fun findFirstByIsClpProjectAndIdNotOrderByIdDesc(isClpProject: Boolean, id: Serializable?): Project? | |||||
} | } |
@@ -9,6 +9,7 @@ interface ProjectSearchInfo { | |||||
val id: Long? | val id: Long? | ||||
val name: String? | val name: String? | ||||
val code: String? | val code: String? | ||||
val status: String? | |||||
@get:Value("#{target.projectCategory.name}") | @get:Value("#{target.projectCategory.name}") | ||||
val category: String? | val category: String? | ||||
@@ -40,8 +40,11 @@ open class ProjectsService( | |||||
private val milestoneRepository: MilestoneRepository, | private val milestoneRepository: MilestoneRepository, | ||||
private val gradeAllocationRepository: GradeAllocationRepository, | private val gradeAllocationRepository: GradeAllocationRepository, | ||||
private val projectTaskRepository: ProjectTaskRepository, | 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> { | open fun allProjects(): List<ProjectSearchInfo> { | ||||
return projectRepository.findProjectSearchInfoByOrderByCreatedDesc() | return projectRepository.findProjectSearchInfoByOrderByCreatedDesc() | ||||
@@ -60,34 +63,42 @@ open class ProjectsService( | |||||
} | } | ||||
open fun markDeleted(id: Long) { | 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> { | open fun allAssignedProjects(): List<AssignedProject> { | ||||
return SecurityUtils.getUser().getOrNull()?.let { user -> | return SecurityUtils.getUser().getOrNull()?.let { user -> | ||||
staffRepository.findByUserId(user.id).getOrNull()?.let { staff -> | staffRepository.findByUserId(user.id).getOrNull()?.let { staff -> | ||||
staffAllocationRepository.findAssignedProjectsByStaff(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() | } ?: emptyList() | ||||
} | } | ||||
@@ -102,10 +113,12 @@ open class ProjectsService( | |||||
milestones = milestoneRepository.findAllByProject(project) | milestones = milestoneRepository.findAllByProject(project) | ||||
.filter { milestone -> milestone.taskGroup?.id != null } | .filter { milestone -> milestone.taskGroup?.id != null } | ||||
.associateBy { milestone -> milestone.taskGroup!!.id!! } | .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() | 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 | @Transactional | ||||
open fun saveProject(request: NewProjectRequest): NewProjectResponse { | open fun saveProject(request: NewProjectRequest): NewProjectResponse { | ||||
val projectCategory = | val projectCategory = | ||||
@@ -124,9 +150,12 @@ open class ProjectsService( | |||||
val location = locationRepository.findById(request.locationId).orElseThrow() | val location = locationRepository.findById(request.locationId).orElseThrow() | ||||
val buildingTypes = buildingTypeRepository.findAllById(request.buildingTypeIds).toMutableSet() | val buildingTypes = buildingTypeRepository.findAllById(request.buildingTypeIds).toMutableSet() | ||||
val workNatures = workNatureRepository.findAllById(request.workNatureIds).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 teamLead = staffRepository.findById(request.projectLeadId).orElseThrow() | ||||
val customer = customerService.findCustomer(request.clientId) | val customer = customerService.findCustomer(request.clientId) | ||||
val subsidiaryContact = subsidiaryContactRepository.findById(request.clientContactId).orElse(null) | |||||
val clientContact = customerContactService.findByContactId(request.clientContactId) | val clientContact = customerContactService.findByContactId(request.clientContactId) | ||||
val customerSubsidiary = request.clientSubsidiaryId?.let { subsidiaryService.findSubsidiary(it) } | val customerSubsidiary = request.clientSubsidiaryId?.let { subsidiaryService.findSubsidiary(it) } | ||||
@@ -134,15 +163,24 @@ open class ProjectsService( | |||||
val taskGroupMap = tasksService.allTaskGroups().associateBy { it.id } | val taskGroupMap = tasksService.allTaskGroups().associateBy { it.id } | ||||
val gradeMap = gradeService.allGrades().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 { | project.apply { | ||||
name = request.projectName | name = request.projectName | ||||
description = request.projectDescription | 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 | expectedTotalFee = request.expectedProjectFee | ||||
totalManhour = request.totalManhour | totalManhour = request.totalManhour | ||||
actualStart = request.projectActualStart | actualStart = request.projectActualStart | ||||
actualEnd = request.projectActualEnd | 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.projectCategory = projectCategory | ||||
this.fundingType = fundingType | this.fundingType = fundingType | ||||
@@ -151,12 +189,13 @@ open class ProjectsService( | |||||
this.location = location | this.location = location | ||||
this.buildingTypes = buildingTypes | this.buildingTypes = buildingTypes | ||||
this.workNatures = workNatures | this.workNatures = workNatures | ||||
this.taskTemplate = taskTemplate | |||||
this.teamLead = teamLead | this.teamLead = teamLead | ||||
this.customer = customer | 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 | this.customerSubsidiary = customerSubsidiary | ||||
} | } | ||||
@@ -165,7 +204,10 @@ open class ProjectsService( | |||||
val tasksToSave = mutableListOf<ProjectTask>() | val tasksToSave = mutableListOf<ProjectTask>() | ||||
val milestones = request.taskGroups.entries.map { (taskStageId, taskGroupAllocation) -> | val milestones = request.taskGroups.entries.map { (taskStageId, taskGroupAllocation) -> | ||||
val taskGroup = taskGroupRepository.findById(taskStageId).orElse(TaskGroup()) ?: TaskGroup() | 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 { | milestone.apply { | ||||
val newMilestone = this | val newMilestone = this | ||||
val requestMilestone = request.milestones[taskStageId] | val requestMilestone = request.milestones[taskStageId] | ||||
@@ -177,7 +219,8 @@ open class ProjectsService( | |||||
this.milestonePayments.removeAll(this.milestonePayments) | this.milestonePayments.removeAll(this.milestonePayments) | ||||
} | } | ||||
requestMilestone?.payments?.map { | 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.milestonePayments.add(milestonePayment.apply { | ||||
this.milestone = newMilestone | this.milestone = newMilestone | ||||
this.description = it.description | this.description = it.description | ||||
@@ -190,7 +233,11 @@ open class ProjectsService( | |||||
this.stagePercentAllocation = taskGroupAllocation.percentAllocation | this.stagePercentAllocation = taskGroupAllocation.percentAllocation | ||||
taskGroupAllocation.taskIds.map { taskId -> | 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 { | projectTask.apply { | ||||
this.project = project | this.project = project | ||||
@@ -205,7 +252,11 @@ open class ProjectsService( | |||||
// Grade allocation (from manhourPercentageByGrade) | // Grade allocation (from manhourPercentageByGrade) | ||||
val gradeAllocations = request.manhourPercentageByGrade.entries.map { | 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 { | gradeAllocation.apply { | ||||
this.project = project | this.project = project | ||||
@@ -225,7 +276,8 @@ open class ProjectsService( | |||||
val milestonesToDelete = milestoneRepository.findAllByProject(project).subtract(milestones.toSet()) | val milestonesToDelete = milestoneRepository.findAllByProject(project).subtract(milestones.toSet()) | ||||
val tasksToDelete = projectTaskRepository.findAllByProject(project).subtract(tasksToSave.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) | milestoneRepository.deleteAll(milestonesToDelete) | ||||
projectTaskRepository.deleteAll(tasksToDelete) | projectTaskRepository.deleteAll(tasksToDelete) | ||||
gradeAllocationRepository.deleteAll(gradeAllocationsToDelete) | gradeAllocationRepository.deleteAll(gradeAllocationsToDelete) | ||||
@@ -236,11 +288,14 @@ open class ProjectsService( | |||||
// Staff allocation | // Staff allocation | ||||
val allocatedStaff = staffRepository.findAllById(request.allocatedStaffIds) | 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.deleteAll(staffAllocationsToDelete) | ||||
staffAllocationRepository.saveAll(staffAllocations) | staffAllocationRepository.saveAll(staffAllocations) | ||||
@@ -260,7 +315,9 @@ open class ProjectsService( | |||||
val project = projectRepository.findById(projectId) | val project = projectRepository.findById(projectId) | ||||
return project.getOrNull()?.let { | 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 | val milestoneMap = it.milestones | ||||
.filter { milestone -> milestone.taskGroup?.id != null } | .filter { milestone -> milestone.taskGroup?.id != null } | ||||
@@ -276,11 +333,14 @@ open class ProjectsService( | |||||
projectLeadId = it.teamLead?.id, | projectLeadId = it.teamLead?.id, | ||||
projectActualStart = it.actualStart, | projectActualStart = it.actualStart, | ||||
projectActualEnd = it.actualEnd, | projectActualEnd = it.actualEnd, | ||||
projectStatus = it.status, | |||||
isClpProject = it.isClpProject, | |||||
serviceTypeId = it.serviceType?.id, | serviceTypeId = it.serviceType?.id, | ||||
fundingTypeId = it.fundingType?.id, | fundingTypeId = it.fundingType?.id, | ||||
contractTypeId = it.contractType?.id, | contractTypeId = it.contractType?.id, | ||||
locationId = it.location?.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 }, | workNatureIds = it.workNatures.mapNotNull { workNature -> workNature.id }, | ||||
clientId = it.customer?.id, | clientId = it.customer?.id, | ||||
clientContactId = customerContact.find { contact -> contact.name == it.custLeadName }?.id, | clientContactId = customerContact.find { contact -> contact.name == it.custLeadName }?.id, | ||||
@@ -292,21 +352,28 @@ open class ProjectsService( | |||||
taskGroups = projectTaskRepository.findAllByProject(it) | taskGroups = projectTaskRepository.findAllByProject(it) | ||||
.mapNotNull { projectTask -> if (projectTask.task?.taskGroup?.id != null) projectTask.task else null } | .mapNotNull { projectTask -> if (projectTask.task?.taskGroup?.id != null) projectTask.task else null } | ||||
.groupBy { task -> task.taskGroup!!.id!! } | .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 | expectedProjectFee = it.expectedTotalFee | ||||
) | ) | ||||
} | } | ||||
@@ -13,6 +13,8 @@ data class EditProjectDetails( | |||||
val projectLeadId: Long?, | val projectLeadId: Long?, | ||||
val projectActualStart: LocalDate?, | val projectActualStart: LocalDate?, | ||||
val projectActualEnd: LocalDate?, | val projectActualEnd: LocalDate?, | ||||
val projectStatus: String?, | |||||
val isClpProject: Boolean?, | |||||
val serviceTypeId: Long?, | val serviceTypeId: Long?, | ||||
val fundingTypeId: Long?, | val fundingTypeId: Long?, | ||||
@@ -20,6 +22,7 @@ data class EditProjectDetails( | |||||
val locationId: Long?, | val locationId: Long?, | ||||
val buildingTypeIds: List<Long>, | val buildingTypeIds: List<Long>, | ||||
val workNatureIds: List<Long>, | val workNatureIds: List<Long>, | ||||
val taskTemplateId: Long?, | |||||
// Client details | // Client details | ||||
val clientId: Long?, | val clientId: Long?, | ||||
@@ -1,12 +1,13 @@ | |||||
package com.ffii.tsms.modules.project.web.models | package com.ffii.tsms.modules.project.web.models | ||||
import com.ffii.tsms.modules.data.entity.SubsidiaryContact | |||||
import jakarta.validation.constraints.NotBlank | import jakarta.validation.constraints.NotBlank | ||||
import java.time.LocalDate | import java.time.LocalDate | ||||
data class NewProjectRequest( | data class NewProjectRequest( | ||||
// Project details | // 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") | @field:NotBlank(message = "project name cannot be empty") | ||||
val projectName: String, | val projectName: String, | ||||
val projectCategoryId: Long, | val projectCategoryId: Long, | ||||
@@ -15,6 +16,7 @@ data class NewProjectRequest( | |||||
val projectId: Long?, | val projectId: Long?, | ||||
val projectActualStart: LocalDate?, | val projectActualStart: LocalDate?, | ||||
val projectActualEnd: LocalDate?, | val projectActualEnd: LocalDate?, | ||||
val isClpProject: Boolean?, | |||||
val serviceTypeId: Long, | val serviceTypeId: Long, | ||||
val fundingTypeId: Long, | val fundingTypeId: Long, | ||||
@@ -22,6 +24,8 @@ data class NewProjectRequest( | |||||
val locationId: Long, | val locationId: Long, | ||||
val buildingTypeIds: List<Long>, | val buildingTypeIds: List<Long>, | ||||
val workNatureIds: List<Long>, | val workNatureIds: List<Long>, | ||||
val taskTemplateId: Long?, | |||||
val isSubsidiaryContact: Boolean?, | |||||
// Client details | // Client details | ||||
val clientId: Long, | val clientId: Long, | ||||
@@ -16,6 +16,7 @@ import java.io.IOException | |||||
import java.time.LocalDate | import java.time.LocalDate | ||||
import java.time.format.DateTimeFormatter | import java.time.format.DateTimeFormatter | ||||
import java.util.* | import java.util.* | ||||
import org.apache.poi.ss.util.CellAddress | |||||
data class DayInfo(val date: String?, val weekday: String?) | data class DayInfo(val date: String?, val weekday: String?) | ||||
@Service | @Service | ||||
@@ -27,6 +28,7 @@ open class ReportService { | |||||
private val PROJECT_CASH_FLOW_REPORT = "templates/report/EX02_Project Cash Flow Report.xlsx" | 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 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 SALART_LIST_TEMPLATE = "templates/report/Salary Template.xlsx" | ||||
private val LATE_START_REPORT = "templates/report/AR01_Late Start Report v01.xlsx" | |||||
// ==============================|| GENERATE REPORT ||============================== // | // ==============================|| GENERATE REPORT ||============================== // | ||||
@@ -78,6 +80,15 @@ open class ReportService { | |||||
return outputStream.toByteArray() | 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 ||============================== // | // ==============================|| CREATE REPORT ||============================== // | ||||
// EX01 Financial Report | // EX01 Financial Report | ||||
@@ -106,7 +117,7 @@ open class ReportService { | |||||
val sheet: Sheet = workbook.getSheetAt(0) | val sheet: Sheet = workbook.getSheetAt(0) | ||||
// accounting style + comma style | // 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 rowIndex = 1 // Assuming the location is in (1,2), which is the report date field | ||||
var columnIndex = 2 | var columnIndex = 2 | ||||
@@ -552,4 +563,75 @@ open class ReportService { | |||||
return workbook | 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.RequestMapping | ||||
import org.springframework.web.bind.annotation.RequestParam | import org.springframework.web.bind.annotation.RequestParam | ||||
import org.springframework.web.bind.annotation.RestController | import org.springframework.web.bind.annotation.RestController | ||||
import org.springframework.http.HttpHeaders | |||||
import org.springframework.http.MediaType | |||||
import java.io.IOException | import java.io.IOException | ||||
import java.time.LocalDate | import java.time.LocalDate | ||||
@@ -93,4 +95,18 @@ class ReportController( | |||||
val project = projectRepository.findById(id).orElseThrow() | val project = projectRepository.findById(id).orElseThrow() | ||||
return invoiceService.findAllByProjectAndPaidAmountIsNotNull(project) | 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`; |