diff --git a/server/src/main/java/io/flexwork/config/AsyncConfiguration.java b/server/src/main/java/io/flexwork/config/AsyncConfiguration.java index be99abac..cd1c354a 100644 --- a/server/src/main/java/io/flexwork/config/AsyncConfiguration.java +++ b/server/src/main/java/io/flexwork/config/AsyncConfiguration.java @@ -1,7 +1,6 @@ package io.flexwork.config; import java.util.concurrent.Executor; -import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; @@ -13,15 +12,12 @@ import org.springframework.core.task.TaskExecutor; import org.springframework.scheduling.annotation.AsyncConfigurer; import org.springframework.scheduling.annotation.EnableAsync; -import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.security.task.DelegatingSecurityContextTaskExecutor; import tech.jhipster.async.ExceptionHandlingAsyncTaskExecutor; @Configuration @EnableAsync -@EnableScheduling -@EnableSchedulerLock(defaultLockAtMostFor = "10m") @Profile("!testdev & !testprod") public class AsyncConfiguration implements AsyncConfigurer { diff --git a/server/src/main/java/io/flexwork/config/SchedulerConfiguration.java b/server/src/main/java/io/flexwork/config/SchedulerConfiguration.java new file mode 100644 index 00000000..6fdd3261 --- /dev/null +++ b/server/src/main/java/io/flexwork/config/SchedulerConfiguration.java @@ -0,0 +1,20 @@ +package io.flexwork.config; + +import javax.sql.DataSource; +import net.javacrumbs.shedlock.core.LockProvider; +import net.javacrumbs.shedlock.provider.jdbctemplate.JdbcTemplateLockProvider; +import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Profile; +import org.springframework.scheduling.annotation.EnableScheduling; + +@EnableScheduling +@EnableSchedulerLock(defaultLockAtMostFor = "10m") +@Profile("!testdev & !testprod") +public class SchedulerConfiguration { + + @Bean + public LockProvider lockProvider(DataSource dataSource) { + return new JdbcTemplateLockProvider(dataSource); + } +} diff --git a/server/src/main/java/io/flexwork/modules/audit/AbstractAuditDTO.java b/server/src/main/java/io/flexwork/modules/audit/AbstractAuditDTO.java new file mode 100644 index 00000000..d3df17c2 --- /dev/null +++ b/server/src/main/java/io/flexwork/modules/audit/AbstractAuditDTO.java @@ -0,0 +1,10 @@ +package io.flexwork.modules.audit; + +import java.time.Instant; +import lombok.Data; + +@Data +public abstract class AbstractAuditDTO { + Instant createdAt; + Instant modifiedAt; +} diff --git a/server/src/main/java/io/flexwork/modules/usermanagement/domain/AbstractAuditingEntity.java b/server/src/main/java/io/flexwork/modules/audit/AbstractAuditingEntity.java similarity index 51% rename from server/src/main/java/io/flexwork/modules/usermanagement/domain/AbstractAuditingEntity.java rename to server/src/main/java/io/flexwork/modules/audit/AbstractAuditingEntity.java index 69513e6c..ff20aaf8 100644 --- a/server/src/main/java/io/flexwork/modules/usermanagement/domain/AbstractAuditingEntity.java +++ b/server/src/main/java/io/flexwork/modules/audit/AbstractAuditingEntity.java @@ -1,12 +1,18 @@ -package io.flexwork.modules.usermanagement.domain; +package io.flexwork.modules.audit; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import io.flexwork.modules.usermanagement.domain.User; import jakarta.persistence.Column; import jakarta.persistence.EntityListeners; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; import jakarta.persistence.MappedSuperclass; import java.io.Serializable; import java.time.Instant; import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; import org.springframework.data.annotation.CreatedBy; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedBy; @@ -18,9 +24,11 @@ * by, last modified by attributes. */ @MappedSuperclass +@SuperBuilder +@NoArgsConstructor @EntityListeners(AuditingEntityListener.class) @JsonIgnoreProperties( - value = {"createdBy", "createdDate", "lastModifiedBy", "lastModifiedDate"}, + value = {"createdBy", "createdAt", "modifiedBy", "modifiedAt"}, allowGetters = true) @Data public abstract class AbstractAuditingEntity<T> implements Serializable { @@ -30,18 +38,26 @@ public abstract class AbstractAuditingEntity<T> implements Serializable { public abstract T getId(); @CreatedBy - @Column(name = "created_by", nullable = false, length = 256, updatable = false) - private String createdBy; + @Column(name = "created_by", updatable = false) + private Long createdBy; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "created_by", insertable = false, updatable = false) + private User createdByUser; @CreatedDate - @Column(name = "created_date", updatable = false) - private Instant createdDate = Instant.now(); + @Column(name = "created_at", updatable = false) + private Instant createdAt = Instant.now(); @LastModifiedBy - @Column(name = "last_modified_by", length = 256) - private String lastModifiedBy; + @Column(name = "modified_by") + private Long modifiedBy; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "modified_by", insertable = false, updatable = false) + private User modifiedByUser; @LastModifiedDate - @Column(name = "last_modified_date") - private Instant lastModifiedDate = Instant.now(); + @Column(name = "modified_at") + private Instant modifiedAt = Instant.now(); } diff --git a/server/src/main/java/io/flexwork/modules/audit/AbstractEntityFieldHandlerRegistry.java b/server/src/main/java/io/flexwork/modules/audit/AbstractEntityFieldHandlerRegistry.java index cc67496e..6c600b9d 100644 --- a/server/src/main/java/io/flexwork/modules/audit/AbstractEntityFieldHandlerRegistry.java +++ b/server/src/main/java/io/flexwork/modules/audit/AbstractEntityFieldHandlerRegistry.java @@ -3,11 +3,10 @@ import io.flexwork.modules.collab.domain.EntityType; import java.util.HashMap; import java.util.Map; -import java.util.function.BiFunction; public abstract class AbstractEntityFieldHandlerRegistry implements EntityFieldHandlerRegistry { - private final Map<String, BiFunction<Object, Object, String>> fieldHandlers = new HashMap<>(); + private final Map<String, EntityFieldHandler> fieldHandlers = new HashMap<>(); /** * Add a field handler for a specific field. @@ -15,7 +14,7 @@ public abstract class AbstractEntityFieldHandlerRegistry implements EntityFieldH * @param fieldName The name of the field. * @param handler A function that processes the old and new values of the field. */ - protected void addFieldHandler(String fieldName, BiFunction<Object, Object, String> handler) { + protected void addFieldHandler(String fieldName, EntityFieldHandler handler) { fieldHandlers.put(fieldName, handler); } @@ -26,7 +25,7 @@ protected void addFieldHandler(String fieldName, BiFunction<Object, Object, Stri * @return A handler function that processes the old and new values of the field. */ @Override - public BiFunction<Object, Object, String> getHandler(String fieldName) { + public EntityFieldHandler getHandler(String fieldName) { return fieldHandlers.get(fieldName); } diff --git a/server/src/main/java/io/flexwork/modules/audit/AuditLogUpdateEvent.java b/server/src/main/java/io/flexwork/modules/audit/AuditLogUpdateEvent.java index f60dbb3a..f4e444f5 100644 --- a/server/src/main/java/io/flexwork/modules/audit/AuditLogUpdateEvent.java +++ b/server/src/main/java/io/flexwork/modules/audit/AuditLogUpdateEvent.java @@ -6,10 +6,12 @@ @Getter public class AuditLogUpdateEvent extends ApplicationEvent { + private final Object previousEntity; private final Object updatedEntity; - public AuditLogUpdateEvent(Object source, Object updatedEntity) { + public AuditLogUpdateEvent(Object source, Object previousEntity, Object updatedEntity) { super(source); + this.previousEntity = previousEntity; this.updatedEntity = updatedEntity; } } diff --git a/server/src/main/java/io/flexwork/modules/audit/AuditLogUpdateEventListener.java b/server/src/main/java/io/flexwork/modules/audit/AuditLogUpdateEventListener.java index e051f26d..aca42469 100644 --- a/server/src/main/java/io/flexwork/modules/audit/AuditLogUpdateEventListener.java +++ b/server/src/main/java/io/flexwork/modules/audit/AuditLogUpdateEventListener.java @@ -4,15 +4,12 @@ import io.flexwork.modules.collab.domain.EntityType; import io.flexwork.modules.collab.repository.ActivityLogRepository; import io.flexwork.security.SecurityUtils; -import jakarta.persistence.EntityNotFoundException; import java.lang.reflect.Field; -import java.lang.reflect.Method; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.event.EventListener; -import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @@ -40,23 +37,17 @@ public AuditLogUpdateEventListener( @EventListener public void onNewTeamRequestCreated(AuditLogUpdateEvent event) { try { - + Object previousEntity = event.getPreviousEntity(); Object updatedEntity = event.getUpdatedEntity(); Class<?> entityClass = updatedEntity.getClass(); Long entityId = extractEntityId(updatedEntity); - // Fetch the existing entity - Object existingEntity = fetchExistingEntity(entityClass, entityId); - - // Convert existing entity to DTO form using the mapper - Object existingEntityDto = mapEntityToDto(existingEntity.getClass(), existingEntity); - // Get the registry for the entity EntityFieldHandlerRegistry registry = registryFactory.getRegistry(entityClass); // Find changes between the existing DTO and updated entity List<AuditUtils.FieldChange> changes = - AuditUtils.findChanges(existingEntityDto, updatedEntity, registry); + AuditUtils.findChanges(previousEntity, updatedEntity, registry); if (!changes.isEmpty()) { // Generate HTML content @@ -71,43 +62,6 @@ public void onNewTeamRequestCreated(AuditLogUpdateEvent event) { } } - private Object mapEntityToDto(Class<?> entityClass, Object entity) { - try { - // Derive the mapper bean name based on conventions (e.g., "teamMapper") - String mapperBeanName = getMapperBeanName(entityClass); - - // Fetch the mapper bean from the Spring context - Object mapper = applicationContext.getBean(mapperBeanName); - - // Find the "toDto" method in the mapper - Method toDtoMethod = mapper.getClass().getDeclaredMethod("toDto", entityClass); - - // Invoke the "toDto" method and return the result - return toDtoMethod.invoke(mapper, entity); - } catch (Exception e) { - throw new RuntimeException( - "Failed to map entity to DTO for class: " + entityClass.getSimpleName(), e); - } - } - - private String getMapperBeanName(Class<?> entityClass) { - // Get the simple name of the entity class - String entityName = entityClass.getSimpleName(); - - // Remove the "DTO" suffix if it exists - if (entityName.endsWith("DTO")) { - entityName = entityName.substring(0, entityName.length() - 3); - } - - // Convert the entity name to the mapper bean name - String mapperBeanName = - Character.toLowerCase(entityName.charAt(0)) - + entityName.substring(1) - + "MapperImpl"; - - return mapperBeanName; - } - private void saveActivityLog(EntityType entityType, Long entityId, String activityDetails) { ActivityLog activityLog = new ActivityLog(); activityLog.setEntityType(entityType); @@ -117,46 +71,6 @@ private void saveActivityLog(EntityType entityType, Long entityId, String activi activityLogRepository.save(activityLog); } - private Object fetchExistingEntity(Class<?> entityClass, Long entityId) { - // Get the repository bean name for the entity - String repositoryBeanName = getRepositoryBeanName(entityClass); - - // Fetch the repository dynamically - JpaRepository<?, Long> repository = - (JpaRepository<?, Long>) applicationContext.getBean(repositoryBeanName); - - // Query the existing entity by ID - return repository - .findById(entityId) - .orElseThrow( - () -> - new EntityNotFoundException( - "Entity of class " - + entityClass.getSimpleName() - + " with ID " - + entityId - + " not found")); - } - - private String getRepositoryBeanName(Class<?> entityClass) { - // Get the simple name of the entity class - String entityName = entityClass.getSimpleName(); - - // Remove "DTO" suffix if present - if (entityName.endsWith("DTO")) { - entityName = entityName.substring(0, entityName.length() - 3); - } - - // Convert the entity name to the repository bean name - // Convention: repositoryBeanName = entityName (lowerCamelCase) + "Repository" - String repositoryBeanName = - Character.toLowerCase(entityName.charAt(0)) - + entityName.substring(1) - + "Repository"; - - return repositoryBeanName; - } - private Long extractEntityId(Object entity) { try { Field idField = entity.getClass().getDeclaredField("id"); diff --git a/server/src/main/java/io/flexwork/modules/audit/AuditUtils.java b/server/src/main/java/io/flexwork/modules/audit/AuditUtils.java index debbcb3c..0fe62749 100644 --- a/server/src/main/java/io/flexwork/modules/audit/AuditUtils.java +++ b/server/src/main/java/io/flexwork/modules/audit/AuditUtils.java @@ -3,7 +3,6 @@ import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; -import java.util.function.BiFunction; public class AuditUtils { @@ -46,54 +45,30 @@ public static List<FieldChange> findChanges( // Compare values and check for changes if ((oldValue == null && newValue != null) || (oldValue != null && !oldValue.equals(newValue))) { - BiFunction<Object, Object, String> handler = registry.getHandler(field.getName()); - String description; + EntityFieldHandler handler = registry.getHandler(field.getName()); - // Use custom handler if available, otherwise provide a default change description + // Only add the fields to audit log if the handler is presented if (handler != null) { - description = handler.apply(oldValue, newValue); - } else { - description = - generateDefaultChangeDescription(field.getName(), oldValue, newValue); + oldValue = handler.getFieldGetter().apply(oldEntity, oldValue); + newValue = handler.getFieldGetter().apply(newEntity, newValue); + changes.add(new FieldChange(handler.getFieldName(), oldValue, newValue)); } - - changes.add(new FieldChange(field.getName(), oldValue, newValue, description)); } } return changes; } - /** - * Generate a default description for a field change when no custom handler is available. - * - * @param fieldName The name of the field. - * @param oldValue The old value of the field. - * @param newValue The new value of the field. - * @return A default string description of the field change. - */ - private static String generateDefaultChangeDescription( - String fieldName, Object oldValue, Object newValue) { - return fieldName - + " changed from '" - + (oldValue != null ? oldValue : "N/A") - + "' to '" - + (newValue != null ? newValue : "N/A") - + "'"; - } - /** Represents a single field change in the audit log. */ public static class FieldChange { private final String fieldName; private final Object oldValue; private final Object newValue; - private final String description; - public FieldChange(String fieldName, Object oldValue, Object newValue, String description) { + public FieldChange(String fieldName, Object oldValue, Object newValue) { this.fieldName = fieldName; this.oldValue = oldValue; this.newValue = newValue; - this.description = description; } public String getFieldName() { @@ -107,9 +82,5 @@ public Object getOldValue() { public Object getNewValue() { return newValue; } - - public String getDescription() { - return description; - } } } diff --git a/server/src/main/java/io/flexwork/modules/audit/EntityFieldHandler.java b/server/src/main/java/io/flexwork/modules/audit/EntityFieldHandler.java new file mode 100644 index 00000000..f914441f --- /dev/null +++ b/server/src/main/java/io/flexwork/modules/audit/EntityFieldHandler.java @@ -0,0 +1,20 @@ +package io.flexwork.modules.audit; + +import java.util.function.BiFunction; +import lombok.Getter; + +@Getter +public class EntityFieldHandler<T> { + + private String fieldName; + private BiFunction<T, Object, String> fieldGetter; + + public EntityFieldHandler(String fieldName) { + this(fieldName, ((objectVal, fieldVal) -> fieldVal == null ? "" : fieldVal.toString())); + } + + public EntityFieldHandler(String displayField, BiFunction<T, Object, String> fieldGetter) { + this.fieldName = displayField; + this.fieldGetter = fieldGetter; + } +} diff --git a/server/src/main/java/io/flexwork/modules/audit/EntityFieldHandlerRegistry.java b/server/src/main/java/io/flexwork/modules/audit/EntityFieldHandlerRegistry.java index 75392312..94b62205 100644 --- a/server/src/main/java/io/flexwork/modules/audit/EntityFieldHandlerRegistry.java +++ b/server/src/main/java/io/flexwork/modules/audit/EntityFieldHandlerRegistry.java @@ -1,7 +1,6 @@ package io.flexwork.modules.audit; import io.flexwork.modules.collab.domain.EntityType; -import java.util.function.BiFunction; public interface EntityFieldHandlerRegistry { @@ -11,7 +10,7 @@ public interface EntityFieldHandlerRegistry { * @param fieldName The name of the field to handle. * @return A handler function that processes the old and new values of the field. */ - BiFunction<Object, Object, String> getHandler(String fieldName); + EntityFieldHandler getHandler(String fieldName); /** * Get the class of the entity this registry handles. diff --git a/server/src/main/java/io/flexwork/modules/collab/domain/Comment.java b/server/src/main/java/io/flexwork/modules/collab/domain/Comment.java index aa839367..f7892674 100644 --- a/server/src/main/java/io/flexwork/modules/collab/domain/Comment.java +++ b/server/src/main/java/io/flexwork/modules/collab/domain/Comment.java @@ -1,32 +1,26 @@ package io.flexwork.modules.collab.domain; -import io.flexwork.modules.usermanagement.domain.User; +import io.flexwork.modules.audit.AbstractAuditingEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; -import jakarta.persistence.FetchType; -import jakarta.persistence.ForeignKey; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.PrePersist; import jakarta.persistence.Table; -import java.time.LocalDateTime; import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; @Entity @Table(name = "fw_comment") @Data -@Builder +@SuperBuilder @NoArgsConstructor @AllArgsConstructor -public class Comment { +public class Comment extends AbstractAuditingEntity<Long> { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -35,31 +29,10 @@ public class Comment { @Column(name = "content", nullable = false, columnDefinition = "TEXT") private String content; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn( - name = "created_by", - nullable = false, - foreignKey = @ForeignKey(name = "fk_comment_user")) - private User createdBy; - - @Column( - name = "created_at", - nullable = false, - columnDefinition = "TIMESTAMPTZ", - updatable = false) - private LocalDateTime createdAt; - @Enumerated(EnumType.STRING) @Column(name = "entity_type", nullable = false, length = 20) private EntityType entityType; @Column(name = "entity_id", nullable = false) private Long entityId; - - @PrePersist - private void prePersist() { - if (createdAt == null) { - createdAt = LocalDateTime.now(); - } - } } diff --git a/server/src/main/java/io/flexwork/modules/collab/service/dto/CommentDTO.java b/server/src/main/java/io/flexwork/modules/collab/service/dto/CommentDTO.java index cf95ad85..6319ac9c 100644 --- a/server/src/main/java/io/flexwork/modules/collab/service/dto/CommentDTO.java +++ b/server/src/main/java/io/flexwork/modules/collab/service/dto/CommentDTO.java @@ -1,20 +1,19 @@ package io.flexwork.modules.collab.service.dto; +import io.flexwork.modules.audit.AbstractAuditDTO; import io.flexwork.modules.collab.domain.EntityType; -import java.time.LocalDateTime; -import lombok.Builder; +import java.time.Instant; import lombok.Data; @Data -@Builder -public class CommentDTO { +public class CommentDTO extends AbstractAuditDTO { private Long id; private String content; private Long createdById; private String createdByName; private String createdByImageUrl; - private LocalDateTime createdAt; + private Instant createdAt; private EntityType entityType; private Long entityId; } diff --git a/server/src/main/java/io/flexwork/modules/collab/service/mapper/CommentMapper.java b/server/src/main/java/io/flexwork/modules/collab/service/mapper/CommentMapper.java index 677b0f6b..4c07da1e 100644 --- a/server/src/main/java/io/flexwork/modules/collab/service/mapper/CommentMapper.java +++ b/server/src/main/java/io/flexwork/modules/collab/service/mapper/CommentMapper.java @@ -8,12 +8,12 @@ @Mapper(componentModel = "spring") public interface CommentMapper { - @Mapping(source = "createdBy.id", target = "createdById") - @Mapping(target = "createdByName", expression = "java(mapFullName(comment.getCreatedBy()))") - @Mapping(source = "createdBy.imageUrl", target = "createdByImageUrl") + @Mapping(source = "createdByUser.id", target = "createdById") + @Mapping(target = "createdByName", expression = "java(mapFullName(comment.getCreatedByUser()))") + @Mapping(source = "createdByUser.imageUrl", target = "createdByImageUrl") CommentDTO toDTO(Comment comment); - @Mapping(source = "createdById", target = "createdBy.id") + @Mapping(source = "createdById", target = "createdByUser.id") Comment toEntity(CommentDTO commentDTO); default String mapFullName(User user) { diff --git a/server/src/main/java/io/flexwork/modules/collab/web/rest/ActivityLogController.java b/server/src/main/java/io/flexwork/modules/collab/web/rest/ActivityLogController.java index 582eb6fb..208c9a8c 100644 --- a/server/src/main/java/io/flexwork/modules/collab/web/rest/ActivityLogController.java +++ b/server/src/main/java/io/flexwork/modules/collab/web/rest/ActivityLogController.java @@ -25,7 +25,7 @@ public ResponseEntity<Page<ActivityLogDTO>> getActivityLogs( @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size, @RequestParam(defaultValue = "createdAt") String sortBy, - @RequestParam(defaultValue = "DESC") String sortDirection) { + @RequestParam(defaultValue = "desc") String sortDirection) { Page<ActivityLogDTO> activityLogs = activityLogService.getActivityLogs( entityType, entityId, page, size, sortBy, sortDirection); diff --git a/server/src/main/java/io/flexwork/modules/fss/domain/FsObject.java b/server/src/main/java/io/flexwork/modules/fss/domain/FsObject.java index 88fc09b8..c0623807 100644 --- a/server/src/main/java/io/flexwork/modules/fss/domain/FsObject.java +++ b/server/src/main/java/io/flexwork/modules/fss/domain/FsObject.java @@ -1,6 +1,6 @@ package io.flexwork.modules.fss.domain; -import io.flexwork.modules.usermanagement.domain.AbstractAuditingEntity; +import io.flexwork.modules.audit.AbstractAuditingEntity; import jakarta.persistence.*; import java.io.Serializable; import java.util.HashSet; diff --git a/server/src/main/java/io/flexwork/modules/teams/domain/EscalationTracking.java b/server/src/main/java/io/flexwork/modules/teams/domain/EscalationTracking.java new file mode 100644 index 00000000..8853b4ec --- /dev/null +++ b/server/src/main/java/io/flexwork/modules/teams/domain/EscalationTracking.java @@ -0,0 +1,39 @@ +package io.flexwork.modules.teams.domain; + +import io.flexwork.modules.usermanagement.domain.User; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import java.time.LocalDateTime; + +@Entity +@Table(name = "fw_escalation_tracking") +public class EscalationTracking { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "team_request_id", nullable = false) + private TeamRequest teamRequest; + + @Column(name = "escalation_level", nullable = false) + private Integer escalationLevel; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "escalated_to_user_id") + private User escalatedToUser; + + @Column( + name = "escalation_time", + nullable = false, + columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP") + private LocalDateTime escalationTime; +} diff --git a/server/src/main/java/io/flexwork/modules/teams/domain/TeamRequest.java b/server/src/main/java/io/flexwork/modules/teams/domain/TeamRequest.java index 12dfc1bb..1650d991 100644 --- a/server/src/main/java/io/flexwork/modules/teams/domain/TeamRequest.java +++ b/server/src/main/java/io/flexwork/modules/teams/domain/TeamRequest.java @@ -1,5 +1,6 @@ package io.flexwork.modules.teams.domain; +import io.flexwork.modules.audit.AbstractAuditingEntity; import io.flexwork.modules.usermanagement.domain.User; import jakarta.persistence.Column; import jakarta.persistence.Convert; @@ -11,10 +12,8 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; -import jakarta.persistence.PrePersist; import jakarta.persistence.Table; import java.time.LocalDate; -import java.time.LocalDateTime; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -26,7 +25,7 @@ @Builder @NoArgsConstructor @AllArgsConstructor -public class TeamRequest { +public class TeamRequest extends AbstractAuditingEntity<Long> { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -49,17 +48,15 @@ public class TeamRequest { private User assignUser; private String requestTitle; + private String requestDescription; - private LocalDateTime createdDate; + private String currentState; @Column(nullable = false, length = 50) @Enumerated(EnumType.STRING) private TeamRequestPriority priority; - @Column(name = "last_updated_time") - private LocalDateTime lastUpdatedTime; - @Column(name = "is_deleted", nullable = false) private boolean isDeleted = false; @@ -76,10 +73,6 @@ public class TeamRequest { @Convert(converter = TicketChannelConverter.class) private TicketChannel channel; - @PrePersist - private void prePersist() { - if (createdDate == null) { - createdDate = LocalDateTime.now(); - } - } + @Column(name = "is_completed", nullable = false) + private boolean isCompleted = false; } diff --git a/server/src/main/java/io/flexwork/modules/teams/domain/Workflow.java b/server/src/main/java/io/flexwork/modules/teams/domain/Workflow.java index 412ddd59..834e96d8 100644 --- a/server/src/main/java/io/flexwork/modules/teams/domain/Workflow.java +++ b/server/src/main/java/io/flexwork/modules/teams/domain/Workflow.java @@ -53,4 +53,22 @@ public class Workflow { @EqualsAndHashCode.Exclude @OneToMany(mappedBy = "workflow", cascade = CascadeType.ALL, orphanRemoval = true) private List<WorkflowTransition> transitions; + + @Column( + name = "level1_escalation_timeout", + nullable = false, + columnDefinition = "INT DEFAULT 1000000") + private Integer level1EscalationTimeout; + + @Column( + name = "level2_escalation_timeout", + nullable = false, + columnDefinition = "INT DEFAULT 1000000") + private Integer level2EscalationTimeout; + + @Column( + name = "level3_escalation_timeout", + nullable = false, + columnDefinition = "INT DEFAULT 1000000") + private Integer level3EscalationTimeout; } diff --git a/server/src/main/java/io/flexwork/modules/teams/domain/WorkflowTransition.java b/server/src/main/java/io/flexwork/modules/teams/domain/WorkflowTransition.java index ac98f562..204dabef 100644 --- a/server/src/main/java/io/flexwork/modules/teams/domain/WorkflowTransition.java +++ b/server/src/main/java/io/flexwork/modules/teams/domain/WorkflowTransition.java @@ -1,5 +1,6 @@ package io.flexwork.modules.teams.domain; +import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; @@ -32,4 +33,7 @@ public class WorkflowTransition { private String targetState; private String eventName; private Long slaDuration; + + @Column(name = "escalate_on_violation", nullable = false) + private Boolean escalateOnViolation; } diff --git a/server/src/main/java/io/flexwork/modules/teams/handler/TeamRequestFieldHandlerRegistry.java b/server/src/main/java/io/flexwork/modules/teams/handler/TeamRequestFieldHandlerRegistry.java index 32eb3db9..eff66a3a 100644 --- a/server/src/main/java/io/flexwork/modules/teams/handler/TeamRequestFieldHandlerRegistry.java +++ b/server/src/main/java/io/flexwork/modules/teams/handler/TeamRequestFieldHandlerRegistry.java @@ -1,14 +1,51 @@ package io.flexwork.modules.teams.handler; import io.flexwork.modules.audit.AbstractEntityFieldHandlerRegistry; +import io.flexwork.modules.audit.EntityFieldHandler; import io.flexwork.modules.collab.domain.EntityType; +import io.flexwork.modules.teams.domain.TicketChannel; import io.flexwork.modules.teams.service.dto.TeamRequestDTO; +import io.flexwork.modules.usermanagement.repository.UserRepository; +import java.util.Optional; import org.springframework.stereotype.Component; @Component public class TeamRequestFieldHandlerRegistry extends AbstractEntityFieldHandlerRegistry { + + private final UserRepository userRepository; + + public TeamRequestFieldHandlerRegistry(UserRepository userRepository) { + this.userRepository = userRepository; + } + @Override - protected void initializeFieldHandlers() {} + protected void initializeFieldHandlers() { + addFieldHandler("priority", new EntityFieldHandler<TeamRequestDTO>("Priority")); + addFieldHandler( + "channel", + new EntityFieldHandler<TeamRequestDTO>( + "Channel", + (objectVal, channel) -> + Optional.ofNullable((TicketChannel) channel) + .map(TicketChannel::getDisplayName) + .orElse(""))); + addFieldHandler( + "estimatedCompletionDate", + new EntityFieldHandler<TeamRequestDTO>("Target Completion Date")); + addFieldHandler( + "actualCompletionDate", + new EntityFieldHandler<TeamRequestDTO>("Actual Completion Date")); + addFieldHandler("currentState", new EntityFieldHandler<TeamRequestDTO>("Current State")); + addFieldHandler( + "assignUserId", + new EntityFieldHandler<>( + "Assigned User", + (objectVal, fieldVal) -> + Optional.ofNullable(fieldVal) + .flatMap(id -> userRepository.findById((Long) id)) + .map(user -> user.getFirstName() + " " + user.getLastName()) + .orElse(""))); + } @Override public Class<?> getEntityClass() { diff --git a/server/src/main/java/io/flexwork/modules/teams/repository/EscalationTrackingRepository.java b/server/src/main/java/io/flexwork/modules/teams/repository/EscalationTrackingRepository.java new file mode 100644 index 00000000..4e53f865 --- /dev/null +++ b/server/src/main/java/io/flexwork/modules/teams/repository/EscalationTrackingRepository.java @@ -0,0 +1,47 @@ +package io.flexwork.modules.teams.repository; + +import io.flexwork.modules.teams.domain.EscalationTracking; +import java.time.LocalDateTime; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface EscalationTrackingRepository extends JpaRepository<EscalationTracking, Long> { + + /** + * Finds the maximum escalation level for a specific team request. + * + * @param teamRequestId The ID of the team request. + * @return The highest escalation level for the given request, or empty if no escalations exist. + */ + @Query( + """ + SELECT MAX(et.escalationLevel) + FROM EscalationTracking et + WHERE et.teamRequest.id = :teamRequestId + """) + Optional<Integer> findMaxEscalationLevel(@Param("teamRequestId") Long teamRequestId); + + /** + * Finds all escalations that occurred before a specific threshold for a given escalation level. + * + * @param teamRequestId The ID of the team request. + * @param escalationLevel The escalation level to filter by. + * @param threshold The timestamp threshold for escalation time. + * @return True if any escalations exist for the given level before the threshold, otherwise + * false. + */ + @Query( + """ + SELECT COUNT(et) > 0 + FROM EscalationTracking et + WHERE et.teamRequest.id = :teamRequestId + AND et.escalationLevel = :escalationLevel + AND et.escalationTime < :threshold + """) + boolean existsEscalationBeforeThreshold( + @Param("teamRequestId") Long teamRequestId, + @Param("escalationLevel") int escalationLevel, + @Param("threshold") LocalDateTime threshold); +} diff --git a/server/src/main/java/io/flexwork/modules/teams/repository/TeamRequestRepository.java b/server/src/main/java/io/flexwork/modules/teams/repository/TeamRequestRepository.java index d35f767a..795c09eb 100644 --- a/server/src/main/java/io/flexwork/modules/teams/repository/TeamRequestRepository.java +++ b/server/src/main/java/io/flexwork/modules/teams/repository/TeamRequestRepository.java @@ -1,6 +1,10 @@ package io.flexwork.modules.teams.repository; import io.flexwork.modules.teams.domain.TeamRequest; +import io.flexwork.modules.teams.service.dto.PriorityDistributionDTO; +import io.flexwork.modules.teams.service.dto.SlaDurationDTO; +import io.flexwork.modules.teams.service.dto.TicketDistributionDTO; +import java.util.List; import java.util.Optional; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -55,4 +59,77 @@ public interface TeamRequestRepository LIMIT 1 """) Optional<TeamRequest> findNextEntity(@Param("requestId") Long requestId); + + @Query( + """ + SELECT new io.flexwork.modules.teams.service.dto.SlaDurationDTO( + tr.sourceState, + tr.targetState, + tr.slaDuration, + tr.eventName + ) + FROM TeamRequest r + JOIN WorkflowTransition tr + ON r.workflow.id = tr.workflow.id + AND r.currentState = tr.sourceState + WHERE r.id = :teamRequestId + """) + List<SlaDurationDTO> findSlaDurationsForCurrentState( + @Param("teamRequestId") Long teamRequestId); + + /** + * Finds all distinct workflow IDs associated with team requests. + * + * @return A list of workflow IDs. + */ + @Query("SELECT DISTINCT r.workflow.id FROM TeamRequest r") + List<Long> findAllWorkflowIds(); + + // Query to count tickets assigned to each team member for a specific team + @Query( + "SELECT new io.flexwork.modules.teams.service.dto.TicketDistributionDTO(" + + "u.id, CONCAT(u.firstName, ' ', u.lastName), COUNT(r.id)) " + + "FROM TeamRequest r " + + "LEFT JOIN User u ON r.assignUser.id = u.id " + + "WHERE r.team.id = :teamId AND r.isCompleted = false AND r.isDeleted = false " + + "GROUP BY u.id, u.firstName, u.lastName") + List<TicketDistributionDTO> findTicketDistributionByTeamId(@Param("teamId") Long teamId); + + @Query( + "SELECT r FROM TeamRequest r " + + "WHERE r.team.id = :teamId AND r.isCompleted = false AND r.isDeleted = false " + + "AND r.assignUser IS NULL " + + "ORDER BY CASE r.priority " + + " WHEN io.flexwork.modules.teams.domain.TeamRequestPriority.Trivial THEN 1 " + + " WHEN io.flexwork.modules.teams.domain.TeamRequestPriority.Low THEN 2 " + + " WHEN io.flexwork.modules.teams.domain.TeamRequestPriority.Medium THEN 3 " + + " WHEN io.flexwork.modules.teams.domain.TeamRequestPriority.High THEN 4 " + + " WHEN io.flexwork.modules.teams.domain.TeamRequestPriority.Critical THEN 5 " + + "END ASC") + Page<TeamRequest> findUnassignedTicketsByTeamIdAsc( + @Param("teamId") Long teamId, Pageable pageable); + + @Query( + "SELECT r FROM TeamRequest r " + + "WHERE r.team.id = :teamId AND r.isCompleted = false AND r.isDeleted = false " + + "AND r.assignUser IS NULL " + + "ORDER BY CASE r.priority " + + " WHEN io.flexwork.modules.teams.domain.TeamRequestPriority.Trivial THEN 1 " + + " WHEN io.flexwork.modules.teams.domain.TeamRequestPriority.Low THEN 2 " + + " WHEN io.flexwork.modules.teams.domain.TeamRequestPriority.Medium THEN 3 " + + " WHEN io.flexwork.modules.teams.domain.TeamRequestPriority.High THEN 4 " + + " WHEN io.flexwork.modules.teams.domain.TeamRequestPriority.Critical THEN 5 " + + "END DESC") + Page<TeamRequest> findUnassignedTicketsByTeamIdDesc( + @Param("teamId") Long teamId, Pageable pageable); + + // Query to count tickets by priority for a specific team + @Query( + "SELECT new io.flexwork.modules.teams.service.dto.PriorityDistributionDTO(" + + "r.priority, COUNT(r.id)) " + + "FROM TeamRequest r " + + "WHERE r.team.id = :teamId AND r.isCompleted = false AND r.isDeleted = false " + + "GROUP BY r.priority") + List<PriorityDistributionDTO> findTicketPriorityDistributionByTeamId( + @Param("teamId") Long teamId); } diff --git a/server/src/main/java/io/flexwork/modules/teams/repository/WorkflowRepository.java b/server/src/main/java/io/flexwork/modules/teams/repository/WorkflowRepository.java index 5508290c..eb42be9d 100644 --- a/server/src/main/java/io/flexwork/modules/teams/repository/WorkflowRepository.java +++ b/server/src/main/java/io/flexwork/modules/teams/repository/WorkflowRepository.java @@ -2,6 +2,7 @@ import io.flexwork.modules.teams.domain.Workflow; import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -20,4 +21,17 @@ public interface WorkflowRepository extends JpaRepository<Workflow, Long> { OR (w.visibility = 'TEAM' AND tws.id IS NOT NULL) """) List<Workflow> findAllWorkflowsByTeam(@Param("teamId") Long teamId); + + @Query( + """ + SELECT CASE + WHEN :level = 1 THEN w.level1EscalationTimeout + WHEN :level = 2 THEN w.level2EscalationTimeout + WHEN :level = 3 THEN w.level3EscalationTimeout + END + FROM Workflow w + WHERE w.id = :workflowId + """) + Optional<Integer> findEscalationTimeoutByLevel( + @Param("workflowId") Long workflowId, @Param("level") int level); } diff --git a/server/src/main/java/io/flexwork/modules/teams/service/EscalationService.java b/server/src/main/java/io/flexwork/modules/teams/service/EscalationService.java new file mode 100644 index 00000000..30635741 --- /dev/null +++ b/server/src/main/java/io/flexwork/modules/teams/service/EscalationService.java @@ -0,0 +1,108 @@ +package io.flexwork.modules.teams.service; + +import io.flexwork.modules.collab.service.NotificationService; +import io.flexwork.modules.teams.domain.TeamRequest; +import io.flexwork.modules.teams.repository.EscalationTrackingRepository; +import io.flexwork.modules.teams.repository.TeamRequestRepository; +import io.flexwork.modules.teams.repository.WorkflowRepository; +import jakarta.persistence.EntityNotFoundException; +import jakarta.transaction.Transactional; +import java.time.LocalDateTime; +import java.util.List; +import org.springframework.stereotype.Service; + +@Service +public class EscalationService { + + private final TeamRequestRepository teamRequestRepository; + private final EscalationTrackingRepository escalationTrackingRepository; + private final WorkflowRepository workflowRepository; + private final NotificationService notificationService; + + public EscalationService( + TeamRequestRepository teamRequestRepository, + EscalationTrackingRepository escalationTrackingRepository, + WorkflowRepository workflowRepository, + NotificationService notificationService) { + this.teamRequestRepository = teamRequestRepository; + this.escalationTrackingRepository = escalationTrackingRepository; + this.workflowRepository = workflowRepository; + this.notificationService = notificationService; + } + + public void escalateTicketsAtLevel(int level) { + List<Long> workflowIds = teamRequestRepository.findAllWorkflowIds(); + + for (Long workflowId : workflowIds) { + // Get the timeout for the escalation level from the workflow + Integer timeoutInMinutes = + workflowRepository + .findEscalationTimeoutByLevel(workflowId, level) + .orElseThrow( + () -> + new IllegalStateException( + "Timeout not configured for escalation level " + + level + + " in workflow " + + workflowId)); + + LocalDateTime escalationTimeThreshold = + LocalDateTime.now().minusMinutes(timeoutInMinutes); + + // Find tickets that have exceeded the timeout for this level + // List<Long> tickets = + // teamRequestRepository.findTicketsExceedingSlaAndLevel( + // workflowId, level, escalationTimeThreshold); + // tickets.forEach(this::escalateIfNoAction); + } + } + + @Transactional + public void escalateIfNoAction(Long teamRequestId) { + TeamRequest teamRequest = + teamRequestRepository + .findById(teamRequestId) + .orElseThrow(() -> new EntityNotFoundException("Team Request not found")); + + int currentLevel = + escalationTrackingRepository.findMaxEscalationLevel(teamRequestId).orElse(0); + + Long nextEscalateUserId = getNextEscalationUser(teamRequest, currentLevel + 1); + if (nextEscalateUserId == null) { + notifyAdminForManualIntervention(teamRequestId); + return; + } + + // Add escalation tracking entry + // EscalationTracking newEscalation = new EscalationTracking(); + // newEscalation.setTeamRequestId(teamRequestId); + // newEscalation.setEscalationLevel(currentLevel + 1); + // newEscalation.setEscalatedToUserId(nextEscalateUserId); + // escalationTrackingRepository.save(newEscalation); + // + // // Notify the next user + // notificationService.notifyEscalation(teamRequestId, nextEscalateUserId, + // currentLevel + 1); + } + + private Long getNextEscalationUser(TeamRequest teamRequest, int escalationLevel) { + // switch (escalationLevel) { + // case 1: + // return teamRequest.getTeam().getManagerId(); + // case 2: + // return teamRequest.getTeam().getOrganization() != null + // ? teamRequest.getTeam().getOrganization().getManagerId() + // : null; + // default: + // return null; // No further escalation available + // } + return null; + } + + private void notifyAdminForManualIntervention(Long teamRequestId) { + // notificationService.notifyAdmin( + // String.format("Team Request %d has reached the final escalation level + // without resolution.", teamRequestId) + // ); + } +} diff --git a/server/src/main/java/io/flexwork/modules/teams/service/TeamRequestService.java b/server/src/main/java/io/flexwork/modules/teams/service/TeamRequestService.java index 76028eb5..f45daf7d 100644 --- a/server/src/main/java/io/flexwork/modules/teams/service/TeamRequestService.java +++ b/server/src/main/java/io/flexwork/modules/teams/service/TeamRequestService.java @@ -8,13 +8,17 @@ import io.flexwork.modules.teams.repository.TeamRequestRepository; import io.flexwork.modules.teams.repository.WorkflowRepository; import io.flexwork.modules.teams.repository.WorkflowStateRepository; +import io.flexwork.modules.teams.service.dto.PriorityDistributionDTO; +import io.flexwork.modules.teams.service.dto.SlaDurationDTO; import io.flexwork.modules.teams.service.dto.TeamRequestDTO; +import io.flexwork.modules.teams.service.dto.TicketDistributionDTO; import io.flexwork.modules.teams.service.event.NewTeamRequestCreatedEvent; import io.flexwork.modules.teams.service.mapper.TeamRequestMapper; import io.flexwork.query.QueryDTO; import jakarta.persistence.EntityManager; import jakarta.persistence.EntityNotFoundException; import jakarta.persistence.PersistenceContext; +import java.util.List; import java.util.Objects; import java.util.Optional; import org.jclouds.rest.ResourceNotFoundException; @@ -115,11 +119,14 @@ public TeamRequestDTO updateTeamRequest(TeamRequestDTO teamRequestDTO) { new ResourceNotFoundException( "TeamRequest not found with id: " + teamRequestDTO.getId())); + TeamRequestDTO previousTeamRequest = teamRequestMapper.toDto(existingTeamRequest); teamRequestMapper.updateEntity(teamRequestDTO, existingTeamRequest); - existingTeamRequest = teamRequestRepository.save(existingTeamRequest); - eventPublisher.publishEvent(new AuditLogUpdateEvent(this, teamRequestDTO)); - return teamRequestMapper.toDto(existingTeamRequest); + TeamRequestDTO savedTeamRequest = + teamRequestMapper.toDto(teamRequestRepository.save(existingTeamRequest)); + eventPublisher.publishEvent( + new AuditLogUpdateEvent(this, previousTeamRequest, teamRequestDTO)); + return savedTeamRequest; } @Transactional @@ -145,4 +152,31 @@ public Optional<TeamRequestDTO> getNextEntity(Long requestId) { public Optional<TeamRequestDTO> getPreviousEntity(Long requestId) { return teamRequestRepository.findPreviousEntity(requestId).map(teamRequestMapper::toDto); } + + public List<SlaDurationDTO> getSlaDurationsForCurrentState(Long teamRequestId) { + return teamRequestRepository.findSlaDurationsForCurrentState(teamRequestId); + } + + // Fetch ticket distribution by team member + public List<TicketDistributionDTO> getTicketDistribution(Long teamId) { + return teamRequestRepository.findTicketDistributionByTeamId(teamId); + } + + // Fetch unassigned tickets + public Page<TeamRequestDTO> getUnassignedTickets( + Long teamId, String sortDirection, Pageable pageable) { + if ("desc".equalsIgnoreCase(sortDirection)) { + return teamRequestRepository + .findUnassignedTicketsByTeamIdDesc(teamId, pageable) + .map(teamRequestMapper::toDto); + } + return teamRequestRepository + .findUnassignedTicketsByTeamIdAsc(teamId, pageable) + .map(teamRequestMapper::toDto); + } + + // Fetch ticket priority distribution + public List<PriorityDistributionDTO> getPriorityDistribution(Long teamId) { + return teamRequestRepository.findTicketPriorityDistributionByTeamId(teamId); + } } diff --git a/server/src/main/java/io/flexwork/modules/teams/service/dto/OrganizationDTO.java b/server/src/main/java/io/flexwork/modules/teams/service/dto/OrganizationDTO.java index bbd136f4..32ac0dce 100644 --- a/server/src/main/java/io/flexwork/modules/teams/service/dto/OrganizationDTO.java +++ b/server/src/main/java/io/flexwork/modules/teams/service/dto/OrganizationDTO.java @@ -1,11 +1,15 @@ package io.flexwork.modules.teams.service.dto; import java.util.Set; -import lombok.Builder; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; @Data -@Builder +@NoArgsConstructor +@AllArgsConstructor +@SuperBuilder public class OrganizationDTO { private Long id; private String name; diff --git a/server/src/main/java/io/flexwork/modules/teams/service/dto/PriorityDistributionDTO.java b/server/src/main/java/io/flexwork/modules/teams/service/dto/PriorityDistributionDTO.java new file mode 100644 index 00000000..0ea9301b --- /dev/null +++ b/server/src/main/java/io/flexwork/modules/teams/service/dto/PriorityDistributionDTO.java @@ -0,0 +1,12 @@ +package io.flexwork.modules.teams.service.dto; + +import io.flexwork.modules.teams.domain.TeamRequestPriority; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class PriorityDistributionDTO { + private TeamRequestPriority priority; + private Long ticketCount; +} diff --git a/server/src/main/java/io/flexwork/modules/teams/service/dto/SlaDurationDTO.java b/server/src/main/java/io/flexwork/modules/teams/service/dto/SlaDurationDTO.java new file mode 100644 index 00000000..a21a9357 --- /dev/null +++ b/server/src/main/java/io/flexwork/modules/teams/service/dto/SlaDurationDTO.java @@ -0,0 +1,15 @@ +package io.flexwork.modules.teams.service.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class SlaDurationDTO { + private String sourceState; + private String targetState; + private Long slaDuration; + private String eventName; +} diff --git a/server/src/main/java/io/flexwork/modules/teams/service/dto/TeamDTO.java b/server/src/main/java/io/flexwork/modules/teams/service/dto/TeamDTO.java index 018bc027..81cf59b6 100644 --- a/server/src/main/java/io/flexwork/modules/teams/service/dto/TeamDTO.java +++ b/server/src/main/java/io/flexwork/modules/teams/service/dto/TeamDTO.java @@ -1,12 +1,14 @@ package io.flexwork.modules.teams.service.dto; import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; @Data +@NoArgsConstructor @AllArgsConstructor -@Builder +@SuperBuilder public class TeamDTO { private Long id; diff --git a/server/src/main/java/io/flexwork/modules/teams/service/dto/TeamRequestDTO.java b/server/src/main/java/io/flexwork/modules/teams/service/dto/TeamRequestDTO.java index 3f3747e7..1627d5b7 100644 --- a/server/src/main/java/io/flexwork/modules/teams/service/dto/TeamRequestDTO.java +++ b/server/src/main/java/io/flexwork/modules/teams/service/dto/TeamRequestDTO.java @@ -1,15 +1,17 @@ package io.flexwork.modules.teams.service.dto; import io.flexwork.modules.teams.domain.TicketChannel; +import java.time.Instant; import java.time.LocalDate; -import java.time.LocalDateTime; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; @Data @NoArgsConstructor @AllArgsConstructor +@SuperBuilder public class TeamRequestDTO { private Long id; private Long teamId; @@ -26,9 +28,11 @@ public class TeamRequestDTO { private String requestTitle; private String requestDescription; private String priority; - private LocalDateTime createdDate; private LocalDate estimatedCompletionDate; private LocalDate actualCompletionDate; private String currentState; private TicketChannel channel; + private boolean isCompleted; + private Instant createdAt; + private Instant modifiedAt; } diff --git a/server/src/main/java/io/flexwork/modules/teams/service/dto/TicketDistributionDTO.java b/server/src/main/java/io/flexwork/modules/teams/service/dto/TicketDistributionDTO.java new file mode 100644 index 00000000..de8b60bf --- /dev/null +++ b/server/src/main/java/io/flexwork/modules/teams/service/dto/TicketDistributionDTO.java @@ -0,0 +1,14 @@ +package io.flexwork.modules.teams.service.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class TicketDistributionDTO { + private Long userId; + private String userName; + private Long ticketCount; +} diff --git a/server/src/main/java/io/flexwork/modules/teams/service/dto/WorkflowDTO.java b/server/src/main/java/io/flexwork/modules/teams/service/dto/WorkflowDTO.java index 7c38658c..67e86ca8 100644 --- a/server/src/main/java/io/flexwork/modules/teams/service/dto/WorkflowDTO.java +++ b/server/src/main/java/io/flexwork/modules/teams/service/dto/WorkflowDTO.java @@ -3,10 +3,12 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; @Data @NoArgsConstructor @AllArgsConstructor +@SuperBuilder public class WorkflowDTO { private Long id; @@ -18,4 +20,10 @@ public class WorkflowDTO { private String description; boolean isGlobal; + + private Integer level1EscalationTimeout; + + private Integer level2EscalationTimeout; + + private Integer level3EscalationTimeout; } diff --git a/server/src/main/java/io/flexwork/modules/teams/service/dto/WorkflowStateDTO.java b/server/src/main/java/io/flexwork/modules/teams/service/dto/WorkflowStateDTO.java index a4f7ce6b..016a1689 100644 --- a/server/src/main/java/io/flexwork/modules/teams/service/dto/WorkflowStateDTO.java +++ b/server/src/main/java/io/flexwork/modules/teams/service/dto/WorkflowStateDTO.java @@ -3,10 +3,12 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; @Data @NoArgsConstructor @AllArgsConstructor +@SuperBuilder public class WorkflowStateDTO { private Long id; private Long workflowId; diff --git a/server/src/main/java/io/flexwork/modules/teams/service/mapper/WorkflowMapper.java b/server/src/main/java/io/flexwork/modules/teams/service/mapper/WorkflowMapper.java index ab3049d0..46186c4a 100644 --- a/server/src/main/java/io/flexwork/modules/teams/service/mapper/WorkflowMapper.java +++ b/server/src/main/java/io/flexwork/modules/teams/service/mapper/WorkflowMapper.java @@ -10,7 +10,7 @@ @Mapper(componentModel = "spring") public interface WorkflowMapper { - @Mapping(source = "owner", target = "global", qualifiedByName = "mapIsGlobal") + @Mapping(source = "owner", target = "isGlobal", qualifiedByName = "mapIsGlobal") WorkflowDTO toDto(Workflow workflow); Workflow toEntity(WorkflowDTO workflowDTO); diff --git a/server/src/main/java/io/flexwork/modules/teams/web/rest/TeamRequestController.java b/server/src/main/java/io/flexwork/modules/teams/web/rest/TeamRequestController.java index 68de1157..93234748 100644 --- a/server/src/main/java/io/flexwork/modules/teams/web/rest/TeamRequestController.java +++ b/server/src/main/java/io/flexwork/modules/teams/web/rest/TeamRequestController.java @@ -1,10 +1,15 @@ package io.flexwork.modules.teams.web.rest; import io.flexwork.modules.teams.service.TeamRequestService; +import io.flexwork.modules.teams.service.dto.PriorityDistributionDTO; +import io.flexwork.modules.teams.service.dto.SlaDurationDTO; import io.flexwork.modules.teams.service.dto.TeamRequestDTO; +import io.flexwork.modules.teams.service.dto.TicketDistributionDTO; import io.flexwork.query.QueryDTO; import jakarta.validation.Valid; +import java.util.List; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -16,6 +21,7 @@ import org.springframework.web.bind.annotation.PutMapping; 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; @RestController @@ -82,4 +88,37 @@ public ResponseEntity<TeamRequestDTO> getPreviousEntity(@PathVariable Long curre .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); } + + @GetMapping("/{teamRequestId}/current-state-slas") + public ResponseEntity<List<SlaDurationDTO>> getSlaDurationsForCurrentState( + @PathVariable Long teamRequestId) { + List<SlaDurationDTO> slaDurations = + teamRequestService.getSlaDurationsForCurrentState(teamRequestId); + return ResponseEntity.ok(slaDurations); + } + + // Endpoint to get ticket distribution for a specific team + @GetMapping("/{teamId}/ticket-distribution") + public List<TicketDistributionDTO> getTicketDistribution(@PathVariable Long teamId) { + return teamRequestService.getTicketDistribution(teamId); + } + + // Endpoint to get unassigned tickets for a specific team + @GetMapping("/{teamId}/unassigned-tickets") + public Page<TeamRequestDTO> getUnassignedTickets( + @PathVariable Long teamId, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size, + @RequestParam(defaultValue = "priority") String sortBy, + @RequestParam(defaultValue = "asc") String sortDirection) { + + Pageable pageable = PageRequest.of(page, size); + return teamRequestService.getUnassignedTickets(teamId, sortDirection, pageable); + } + + // Endpoint to get priority distribution for a specific team + @GetMapping("/{teamId}/priority-distribution") + public List<PriorityDistributionDTO> getPriorityDistribution(@PathVariable Long teamId) { + return teamRequestService.getPriorityDistribution(teamId); + } } diff --git a/server/src/main/java/io/flexwork/modules/usermanagement/domain/Tenant.java b/server/src/main/java/io/flexwork/modules/usermanagement/domain/Tenant.java index 4efd75e8..59414ded 100644 --- a/server/src/main/java/io/flexwork/modules/usermanagement/domain/Tenant.java +++ b/server/src/main/java/io/flexwork/modules/usermanagement/domain/Tenant.java @@ -2,6 +2,7 @@ import static io.flexwork.db.DbConstants.MASTER_SCHEMA; +import io.flexwork.modules.audit.AbstractAuditingEntity; import jakarta.persistence.*; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; diff --git a/server/src/main/java/io/flexwork/modules/usermanagement/domain/User.java b/server/src/main/java/io/flexwork/modules/usermanagement/domain/User.java index bf0b1c6b..7c1bce71 100644 --- a/server/src/main/java/io/flexwork/modules/usermanagement/domain/User.java +++ b/server/src/main/java/io/flexwork/modules/usermanagement/domain/User.java @@ -1,6 +1,7 @@ package io.flexwork.modules.usermanagement.domain; import com.fasterxml.jackson.annotation.JsonIgnore; +import io.flexwork.modules.audit.AbstractAuditingEntity; import io.flexwork.modules.teams.domain.Team; import jakarta.persistence.*; import jakarta.validation.constraints.Email; diff --git a/server/src/main/java/io/flexwork/modules/usermanagement/repository/UserRepository.java b/server/src/main/java/io/flexwork/modules/usermanagement/repository/UserRepository.java index 80503d4f..612f8989 100644 --- a/server/src/main/java/io/flexwork/modules/usermanagement/repository/UserRepository.java +++ b/server/src/main/java/io/flexwork/modules/usermanagement/repository/UserRepository.java @@ -2,7 +2,6 @@ import io.flexwork.modules.usermanagement.domain.User; import jakarta.transaction.Transactional; -import java.time.Instant; import java.time.LocalDateTime; import java.util.List; import java.util.Optional; @@ -21,9 +20,6 @@ public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificat Optional<User> findOneByActivationKey(String activationKey); - List<User> findAllByActivatedIsFalseAndActivationKeyIsNotNullAndCreatedDateBefore( - Instant dateTime); - Optional<User> findOneByResetKey(String resetKey); Optional<User> findOneByEmailIgnoreCase(String email); diff --git a/server/src/main/java/io/flexwork/modules/usermanagement/service/UserService.java b/server/src/main/java/io/flexwork/modules/usermanagement/service/UserService.java index e4cd4011..9b65096e 100644 --- a/server/src/main/java/io/flexwork/modules/usermanagement/service/UserService.java +++ b/server/src/main/java/io/flexwork/modules/usermanagement/service/UserService.java @@ -325,24 +325,6 @@ public Optional<UserDTO> getUserWithAuthorities() { .map(userMapper::toDto); } - // - // /** - // * Not activated users should be automatically deleted after 3 days. - // * - // * <p>This is scheduled to get fired everyday, at 01:00 (am). - // */ - // @Scheduled(cron = "0 0 1 * * ?") - // public void removeNotActivatedUsers() { - // userRepository - // .findAllByActivatedIsFalseAndActivationKeyIsNotNullAndCreatedDateBefore( - // Instant.now().minus(3, ChronoUnit.DAYS)) - // .forEach( - // user -> { - // log.debug("Deleting not activated user {}", user.getLogin()); - // userRepository.delete(user); - // }); - // } - public List<ResourcePermissionDTO> getResourcesWithPermissionsByUserId(Long userId) { List<Object[]> results = userRepository.findResourcesWithHighestPermissionsByUserId(userId); diff --git a/server/src/main/java/io/flexwork/modules/usermanagement/service/dto/UserDTO.java b/server/src/main/java/io/flexwork/modules/usermanagement/service/dto/UserDTO.java index ff33bef9..15c37b61 100644 --- a/server/src/main/java/io/flexwork/modules/usermanagement/service/dto/UserDTO.java +++ b/server/src/main/java/io/flexwork/modules/usermanagement/service/dto/UserDTO.java @@ -1,6 +1,7 @@ package io.flexwork.modules.usermanagement.service.dto; import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; import java.io.Serializable; import java.time.Instant; import java.time.LocalDateTime; @@ -17,9 +18,9 @@ public class UserDTO implements Serializable { @Email @EqualsAndHashCode.Include private String email; - private String firstName; + @NotBlank private String firstName; - private String lastName; + @NotBlank private String lastName; private String timezone; @@ -51,11 +52,11 @@ public class UserDTO implements Serializable { private String title; - private String createdBy; + private Long createdBy; - private Instant createdDate; + private Instant createdAt; - private String lastModifiedBy; + private Long lastModifiedBy; - private Instant lastModifiedDate; + private Instant modifiedAt; } diff --git a/server/src/main/java/io/flexwork/modules/usermanagement/web/rest/UserController.java b/server/src/main/java/io/flexwork/modules/usermanagement/web/rest/UserController.java index e831f3b7..55782419 100644 --- a/server/src/main/java/io/flexwork/modules/usermanagement/web/rest/UserController.java +++ b/server/src/main/java/io/flexwork/modules/usermanagement/web/rest/UserController.java @@ -85,9 +85,9 @@ public class UserController { "activated", "langKey", "createdBy", - "createdDate", - "lastModifiedBy", - "lastModifiedDate"); + "createdAt", + "modifiedBy", + "modifiedAt"); private static final Logger LOG = LoggerFactory.getLogger(UserController.class); diff --git a/server/src/main/java/io/flexwork/security/SpringSecurityAuditorAware.java b/server/src/main/java/io/flexwork/security/SpringSecurityAuditorAware.java index 262c8855..afea6263 100644 --- a/server/src/main/java/io/flexwork/security/SpringSecurityAuditorAware.java +++ b/server/src/main/java/io/flexwork/security/SpringSecurityAuditorAware.java @@ -7,12 +7,10 @@ /** Implementation of {@link AuditorAware} based on Spring Security. */ @Component -public class SpringSecurityAuditorAware implements AuditorAware<String> { +public class SpringSecurityAuditorAware implements AuditorAware<Long> { @Override - public Optional<String> getCurrentAuditor() { - return SecurityUtils.getCurrentUserLogin() - .map(UserKey::getEmail) - .or(() -> Optional.of(Constants.SYSTEM)); + public Optional<Long> getCurrentAuditor() { + return SecurityUtils.getCurrentUserLogin().map(UserKey::getId).or(() -> Optional.of(null)); } } diff --git a/server/src/test/java/io/flexwork/service/UserServiceIT.java b/server/src/test/java/io/flexwork/service/UserServiceIT.java index 2914848d..e621defd 100644 --- a/server/src/test/java/io/flexwork/service/UserServiceIT.java +++ b/server/src/test/java/io/flexwork/service/UserServiceIT.java @@ -156,54 +156,6 @@ void assertThatUserCanResetPassword() { userRepository.delete(user); } - // @Test - // @Transactional - // void assertThatNotActivatedUsersWithNotNullActivationKeyCreatedBefore3DaysAreDeleted() { - // Instant now = Instant.now(); - // when(dateTimeProvider.getNow()).thenReturn(Optional.of(now.minus(4, - // ChronoUnit.DAYS))); - // user.setActivated(false); - // user.setActivationKey(RandomStringUtils.random(20)); - // User dbUser = userRepository.saveAndFlush(user); - // dbUser.setCreatedDate(now.minus(4, ChronoUnit.DAYS)); - // userRepository.saveAndFlush(user); - // Instant threeDaysAgo = now.minus(3, ChronoUnit.DAYS); - // List<User> users = - // userRepository - // - // .findAllByActivatedIsFalseAndActivationKeyIsNotNullAndCreatedDateBefore( - // threeDaysAgo); - // assertThat(users).isNotEmpty(); - // userService.removeNotActivatedUsers(); - // users = - // userRepository - // - // .findAllByActivatedIsFalseAndActivationKeyIsNotNullAndCreatedDateBefore( - // threeDaysAgo); - // assertThat(users).isEmpty(); - // } - - // @Test - // @Transactional - // void assertThatNotActivatedUsersWithNullActivationKeyCreatedBefore3DaysAreNotDeleted() { - // Instant now = Instant.now(); - // when(dateTimeProvider.getNow()).thenReturn(Optional.of(now.minus(4, - // ChronoUnit.DAYS))); - // user.setActivated(false); - // User dbUser = userRepository.saveAndFlush(user); - // dbUser.setCreatedDate(now.minus(4, ChronoUnit.DAYS)); - // userRepository.saveAndFlush(user); - // Instant threeDaysAgo = now.minus(3, ChronoUnit.DAYS); - // List<User> users = - // userRepository - // - // .findAllByActivatedIsFalseAndActivationKeyIsNotNullAndCreatedDateBefore( - // threeDaysAgo); - // assertThat(users).isEmpty(); - // userService.removeNotActivatedUsers(); - // Optional<User> maybeDbUser = userRepository.findById(dbUser.getId()); - // assertThat(maybeDbUser).contains(dbUser); - // } @Test @Transactional void testFindResourcesWithHighestPermissionsByUserId() { diff --git a/server/src/test/java/io/flexwork/web/rest/UserControllerIT.java b/server/src/test/java/io/flexwork/web/rest/UserControllerIT.java index b1c3ce4d..56d9a4f0 100644 --- a/server/src/test/java/io/flexwork/web/rest/UserControllerIT.java +++ b/server/src/test/java/io/flexwork/web/rest/UserControllerIT.java @@ -337,9 +337,9 @@ void updateUser() throws Exception { userDTO.setImageUrl(UPDATED_IMAGEURL); userDTO.setLangKey(UPDATED_LANGKEY); userDTO.setCreatedBy(updatedUser.getCreatedBy()); - userDTO.setCreatedDate(updatedUser.getCreatedDate()); - userDTO.setLastModifiedBy(updatedUser.getLastModifiedBy()); - userDTO.setLastModifiedDate(updatedUser.getLastModifiedDate()); + userDTO.setCreatedAt(updatedUser.getCreatedAt()); + userDTO.setLastModifiedBy(updatedUser.getModifiedBy()); + userDTO.setModifiedAt(updatedUser.getModifiedAt()); userDTO.setAuthorities( Collections.singleton(new AuthorityDTO(AuthoritiesConstants.USER, "User"))); return userDTO; @@ -364,9 +364,9 @@ void updateUserLogin() throws Exception { userDTO.setImageUrl(UPDATED_IMAGEURL); userDTO.setLangKey(UPDATED_LANGKEY); userDTO.setCreatedBy(updatedUser.getCreatedBy()); - userDTO.setCreatedDate(updatedUser.getCreatedDate()); - userDTO.setLastModifiedBy(updatedUser.getLastModifiedBy()); - userDTO.setLastModifiedDate(updatedUser.getLastModifiedDate()); + userDTO.setCreatedAt(updatedUser.getCreatedAt()); + userDTO.setLastModifiedBy(updatedUser.getModifiedBy()); + userDTO.setModifiedAt(updatedUser.getModifiedAt()); userDTO.setAuthorities( Collections.singleton(new AuthorityDTO(AuthoritiesConstants.USER, "User"))); @@ -434,9 +434,9 @@ void updateUserExistingEmail() throws Exception { userDTO.setImageUrl(updatedUser.getImageUrl()); userDTO.setLangKey(updatedUser.getLangKey()); userDTO.setCreatedBy(updatedUser.getCreatedBy()); - userDTO.setCreatedDate(updatedUser.getCreatedDate()); - userDTO.setLastModifiedBy(updatedUser.getLastModifiedBy()); - userDTO.setLastModifiedDate(updatedUser.getLastModifiedDate()); + userDTO.setCreatedAt(updatedUser.getCreatedAt()); + userDTO.setLastModifiedBy(updatedUser.getModifiedBy()); + userDTO.setModifiedAt(updatedUser.getModifiedAt()); userDTO.setAuthorities( Collections.singleton(new AuthorityDTO(AuthoritiesConstants.USER, "User"))); diff --git a/tools/liquibase/src/main/resources/config/liquibase/master/changelog/00000000000000_initial_schema.xml b/tools/liquibase/src/main/resources/config/liquibase/master/changelog/00000000000000_initial_schema.xml index d8d00e01..88d68eeb 100644 --- a/tools/liquibase/src/main/resources/config/liquibase/master/changelog/00000000000000_initial_schema.xml +++ b/tools/liquibase/src/main/resources/config/liquibase/master/changelog/00000000000000_initial_schema.xml @@ -31,12 +31,12 @@ <column name="domain" type="varchar(256)"> <constraints nullable="false" unique="true" /> </column> - <column name="created_by" type="varchar(50)"> + <column name="created_by" type="bigint"> <constraints nullable="true" /> </column> - <column name="created_date" type="timestamptz" /> - <column name="last_modified_by" type="varchar(50)" /> - <column name="last_modified_date" type="timestamptz" /> + <column name="created_at" type="timestamptz" /> + <column name="modified_by" type="bigint" /> + <column name="modified_at" type="timestamptz" /> </createTable> <addAutoIncrement tableName="fw_tenant" @@ -48,7 +48,6 @@ <column name="name_id" value="flexwork" /> <column name="description" value="Default tenant" /> <column name="domain" value="" /> - <column name="created_by" value="Hai Nguyen" /> </insert> </changeSet> </databaseChangeLog> \ No newline at end of file diff --git a/tools/liquibase/src/main/resources/config/liquibase/tenant/changelog/00000000000000_initial_schema.xml b/tools/liquibase/src/main/resources/config/liquibase/tenant/changelog/00000000000000_initial_schema.xml index d20ce3b7..19437d37 100644 --- a/tools/liquibase/src/main/resources/config/liquibase/tenant/changelog/00000000000000_initial_schema.xml +++ b/tools/liquibase/src/main/resources/config/liquibase/tenant/changelog/00000000000000_initial_schema.xml @@ -68,15 +68,13 @@ <column name="about" type="TEXT"> <constraints nullable="true" /> </column> - <column name="created_by" type="VARCHAR(256)"> - <constraints nullable="false" /> - </column> - <column name="created_date" type="timestamptz" /> <column name="reset_date" type="timestamptz"> <constraints nullable="true" /> </column> - <column name="last_modified_by" type="VARCHAR(256)" /> - <column name="last_modified_date" type="timestamptz" /> + <column name="created_by" type="bigint" /> + <column name="created_at" type="timestamptz" /> + <column name="modified_by" type="bigint" /> + <column name="modified_at" type="timestamptz" /> </createTable> <addAutoIncrement tableName="fw_user" columnName="id" @@ -228,17 +226,16 @@ <column name="content" type="TEXT"> <constraints nullable="false" /> </column> - <column name="created_by" type="BIGINT"> - <constraints nullable="false" /> - </column> - <column name="created_at" type="timestamptz" - defaultValueComputed="CURRENT_TIMESTAMP" /> <column name="entity_type" type="VARCHAR(20)"> <constraints nullable="false" /> </column> <column name="entity_id" type="BIGINT"> <constraints nullable="false" /> </column> + <column name="created_by" type="bigint" /> + <column name="created_at" type="timestamptz" /> + <column name="modified_by" type="bigint" /> + <column name="modified_at" type="timestamptz" /> </createTable> <addForeignKeyConstraint @@ -333,11 +330,11 @@ <column name="lang_key" type="STRING" /> <column name="activation_key" type="STRING" /> <column name="reset_key" type="STRING" /> - <column name="created_by" type="STRING" /> + <column name="created_by" type="NUMBER" /> <column name="created_date" type="TIMESTAMP" /> <column name="reset_date" type="TIMESTAMP" /> - <column name="last_modified_by" type="STRING" /> - <column name="last_modified_date" type="TIMESTAMP" /> + <column name="modified_by" type="NUMBER" /> + <column name="modified_at" type="TIMESTAMP" /> <column name="about" type="STRING" /> <column name="address" type="STRING" /> <column name="city" type="STRING" /> diff --git a/tools/liquibase/src/main/resources/config/liquibase/tenant/changelog/00000000000002_fss_tables.xml b/tools/liquibase/src/main/resources/config/liquibase/tenant/changelog/00000000000002_fss_tables.xml index 0662cbc1..bc94bfca 100644 --- a/tools/liquibase/src/main/resources/config/liquibase/tenant/changelog/00000000000002_fss_tables.xml +++ b/tools/liquibase/src/main/resources/config/liquibase/tenant/changelog/00000000000002_fss_tables.xml @@ -21,12 +21,14 @@ <column name="type" type="varchar(20)"> <constraints nullable="true" /> </column> - <column name="created_by" type="varchar(256)"> + <column name="created_by" type="bigint"> <constraints nullable="true" /> </column> - <column name="created_date" type="timestamptz" /> - <column name="last_modified_by" type="varchar(50)" /> - <column name="last_modified_date" type="timestamptz" /> + <column name="created_at" type="timestamptz" /> + <column name="modified_by" type="bigint"> + <constraints nullable="true" /> + </column> + <column name="modified_at" type="timestamptz" /> </createTable> diff --git a/tools/liquibase/src/main/resources/config/liquibase/tenant/changelog/00000000000004_request_workflow_tables.xml b/tools/liquibase/src/main/resources/config/liquibase/tenant/changelog/00000000000004_request_workflow_tables.xml index 436299b9..e6badb72 100644 --- a/tools/liquibase/src/main/resources/config/liquibase/tenant/changelog/00000000000004_request_workflow_tables.xml +++ b/tools/liquibase/src/main/resources/config/liquibase/tenant/changelog/00000000000004_request_workflow_tables.xml @@ -34,6 +34,23 @@ defaultValue="PRIVATE"> <constraints nullable="false" /> </column> + + <!--Default value is set to a large number (1000,000 minutes). Configure + this to enable escalations for Levels. --> + <column name="level1_escalation_timeout" type="INT" + defaultValue="1000000"> + <constraints nullable="false" /> + </column> + + <column name="level2_escalation_timeout" type="INT" + defaultValue="1000000"> + <constraints nullable="false" /> + </column> + + <column name="level3_escalation_timeout" type="INT" + defaultValue="1000000"> + <constraints nullable="false" /> + </column> </createTable> <!-- Add foreign key constraint to fw_team for owner_id --> @@ -90,13 +107,6 @@ <column name="priority" type="VARCHAR(50)"> <constraints nullable="false" /> </column> - <column name="created_date" type="TIMESTAMP" - defaultValueComputed="CURRENT_TIMESTAMP"> - <constraints nullable="false" /> - </column> - <column name="last_updated_time" type="TIMESTAMP"> - <constraints nullable="true" /> - </column> <column name="is_deleted" type="BOOLEAN" defaultValue="false"> <constraints nullable="false" /> </column> @@ -107,15 +117,25 @@ </column> <!-- Estimated completion date --> - <column name="estimated_completion_date" type="TIMESTAMP"> + <column name="estimated_completion_date" type="timestamptz"> <constraints nullable="true" /> </column> <!-- Actual completion date --> - <column name="actual_completion_date" type="TIMESTAMP"> + <column name="actual_completion_date" type="timestamptz"> <constraints nullable="true" /> </column> <column name="current_state" type="VARCHAR(255)" /> + <!--This column is set to true if the current_state is associated with + a workflow_state where the is_final flag is true. --> + <column name="is_completed" type="BOOLEAN" + defaultValue="false"> + <constraints nullable="false" /> + </column> + <column name="created_by" type="bigint" /> + <column name="created_at" type="timestamptz" /> + <column name="modified_by" type="bigint" /> + <column name="modified_at" type="timestamptz" /> </createTable> <addForeignKeyConstraint @@ -176,6 +196,10 @@ <constraints nullable="false" /> </column> <column name="sla_duration" type="BIGINT" /> + <column name="escalate_on_violation" type="BOOLEAN" + defaultValue="true"> + <constraints nullable="false" /> + </column> </createTable> <addForeignKeyConstraint baseTableName="fw_workflow_transition" baseColumnNames="workflow_id" @@ -220,11 +244,11 @@ <column name="event_name" type="VARCHAR(255)"> <constraints nullable="false" /> </column> - <column name="transition_date" type="TIMESTAMP" + <column name="transition_date" type="timestamptz" defaultValueComputed="CURRENT_TIMESTAMP"> <constraints nullable="false" /> </column> - <column name="sla_due_date" type="TIMESTAMP" /> + <column name="sla_due_date" type="timestamptz" /> <column name="status" type="VARCHAR(50)" /> </createTable> <addForeignKeyConstraint @@ -232,6 +256,35 @@ baseColumnNames="team_request_id" constraintName="fk_workflow_transition_request" referencedTableName="fw_team_request" referencedColumnNames="id" /> + + <createTable tableName="fw_escalation_tracking"> + <column name="id" type="BIGINT" autoIncrement="true"> + <constraints primaryKey="true" nullable="false" /> + </column> + <column name="team_request_id" type="BIGINT"> + <constraints nullable="false" /> + </column> + <column name="escalation_level" type="INT"> + <constraints nullable="false" /> + </column> + <column name="escalated_to_user_id" type="BIGINT" /> + <column name="escalation_time" type="timestamptz" + defaultValueComputed="CURRENT_TIMESTAMP"> + <constraints nullable="false" /> + </column> + </createTable> + + <addForeignKeyConstraint + baseTableName="fw_escalation_tracking" + baseColumnNames="team_request_id" + referencedTableName="fw_team_request" referencedColumnNames="id" + constraintName="fw_escalation_tracking_request" onDelete="CASCADE" /> + + <addForeignKeyConstraint + baseTableName="fw_escalation_tracking" + baseColumnNames="escalated_to_user_id" + constraintName="fw_escalation_tracking_user" + referencedTableName="fw_user" referencedColumnNames="id" /> </changeSet> <changeSet author="flexapp" id="00000000000004:01-insert-workflow-data"> diff --git a/tools/liquibase/src/main/resources/config/liquibase/tenant/data/fw_user.csv b/tools/liquibase/src/main/resources/config/liquibase/tenant/data/fw_user.csv index 201cd1a7..a88c4707 100644 --- a/tools/liquibase/src/main/resources/config/liquibase/tenant/data/fw_user.csv +++ b/tools/liquibase/src/main/resources/config/liquibase/tenant/data/fw_user.csv @@ -1,19 +1,19 @@ -id;password_hash;first_name;last_name;email;timezone;last_login_time;image_url;role;title;manager_id;activated;lang_key;activation_key;reset_key;created_by;created_date;reset_date;last_modified_by;last_modified_date;about;address;city;state;country -1;$2a$10$gSAhZrxMllrbgj/kkK9UceBPpChGWJA7SYIb1Mqo.n5aNLq1/oRrC;John;Doe;admin@theflexwork.io;America/Los_Angeles;;;USER;Software Engineer;;True;en;ACT123;RST123;system;;;system;;Experienced software engineer;123 Main St;San Francisco;CA;US -2;$2a$10$ItVdBLylXMw3pnv/xkf5qOzrTPos7UngeqVGiP9Z3O7WFBAp7ULry;Jane;Smith;user@theflexwork.io;America/New_York;;;ADMIN;Product Manager;1.0;True;en;ACT456;RST456;system;;;system;;Product manager with 10 years of experience;456 Oak St;New York;NY;US -3;$2a$10$ItVdBLylXMw3pnv/xkf5qOzrTPos7UngeqVGiP9Z3O7WFBAp7ULry;Alice;Johnson;alice.johnson@theflexwork.io;America/Chicago;;;USER;UX Designer;2.0;False;en;ACT789;RST789;system;;;system;;Creative UX designer;789 Pine Ave;Chicago;IL;US -4;$2a$10$ItVdBLylXMw3pnv/xkf5qOzrTPos7UngeqVGiP9Z3O7WFBAp7ULry;Bob;Brown;bob.brown@theflexwork.io;America/Los_Angeles;;;MANAGER;Team Lead;1.0;True;en;ACT321;RST321;system;;;system;;Experienced team leader;101 Maple Dr;Los Angeles;CA;US -5;$2a$10$ItVdBLylXMw3pnv/xkf5qOzrTPos7UngeqVGiP9Z3O7WFBAp7ULry;Charlie;Green;charlie.green@theflexwork.io;America/New_York;;;USER;QA Engineer;2.0;True;en;ACT654;RST654;system;;;system;;Quality assurance specialist;202 Birch Ln;Boston;MA;US -6;$2a$10$ItVdBLylXMw3pnv/xkf5qOzrTPos7UngeqVGiP9Z3O7WFBAp7ULry;Diana;Prince;diana.prince@theflexwork.io;America/Denver;;;USER;Project Coordinator;3.0;False;en;ACT987;RST987;system;;;system;;Project coordinator;303 Cedar Rd;Denver;CO;US -7;$2a$10$ItVdBLylXMw3pnv/xkf5qOzrTPos7UngeqVGiP9Z3O7WFBAp7ULry;Edward;Nash;edward.nash@theflexwork.io;America/Los_Angeles;;;USER;Data Analyst;3.0;True;en;ACT741;RST741;system;;;system;;Data analyst expert;404 Elm St;Seattle;WA;US -8;$2a$10$ItVdBLylXMw3pnv/xkf5qOzrTPos7UngeqVGiP9Z3O7WFBAp7ULry;Fiona;White;fiona.white@theflexwork.io;America/Chicago;;;USER;Marketing Specialist;4.0;True;en;ACT852;RST852;system;;;system;;Specialist in digital marketing;505 Walnut St;Austin;TX;US -9;$2a$10$ItVdBLylXMw3pnv/xkf5qOzrTPos7UngeqVGiP9Z3O7WFBAp7ULry;George;King;george.king@theflexwork.io;America/New_York;;;ADMIN;IT Manager;;False;en;ACT963;RST963;system;;;system;;IT manager;606 Cherry Ave;New York;NY;US -10;$2a$10$ItVdBLylXMw3pnv/xkf5qOzrTPos7UngeqVGiP9Z3O7WFBAp7ULry;Hannah;Moore;hannah.moore@theflexwork.io;America/Denver;;;USER;HR Specialist;5.0;True;en;ACT159;RST159;system;;;system;;Human resources expert;707 Chestnut Dr;Denver;CO;US -11;$2a$10$ItVdBLylXMw3pnv/xkf5qOzrTPos7UngeqVGiP9Z3O7WFBAp7ULry;Isaac;Young;isaac.young@theflexwork.io;America/Chicago;;;USER;Sales Associate;6.0;True;en;ACT753;RST753;system;;;system;;Sales associate;808 Ash St;Chicago;IL;US -12;$2a$10$ItVdBLylXMw3pnv/xkf5qOzrTPos7UngeqVGiP9Z3O7WFBAp7ULry;Julia;Reed;julia.reed@theflexwork.io;America/Chicago;;;USER;Product Designer;5.0;True;en;ACT369;RST369;system;;;system;;Product designer;909 Poplar St;Portland;OR;US -13;$2a$10$ItVdBLylXMw3pnv/xkf5qOzrTPos7UngeqVGiP9Z3O7WFBAp7ULry;Kevin;Wright;kevin.wright@theflexwork.io;America/Chicago;;;USER;Software Engineer;5.0;True;en;ACT258;RST258;system;;;system;;Software engineer;1010 Fir Ave;San Diego;CA;US -14;$2a$10$ItVdBLylXMw3pnv/xkf5qOzrTPos7UngeqVGiP9Z3O7WFBAp7ULry;Liam;Harris;liam.harris@theflexwork.io;America/New_York;;;USER;Support Engineer;6.0;True;en;ACT147;RST147;system;;;system;;Support engineer;1111 Cypress Dr;Philadelphia;PA;US -15;$2a$10$ItVdBLylXMw3pnv/xkf5qOzrTPos7UngeqVGiP9Z3O7WFBAp7ULry;Mia;Clark;mia.clark@theflexwork.io;America/Los_Angeles;;;USER;Business Analyst;;True;en;ACT369;RST369;system;;;system;;Business analyst;1212 Redwood Ln;Los Angeles;CA;US -16;$2a$10$ItVdBLylXMw3pnv/xkf5qOzrTPos7UngeqVGiP9Z3O7WFBAp7ULry;Noah;Lewis;noah.lewis@theflexwork.io;America/Denver;;;USER;Operations Manager;7.0;False;en;ACT951;RST951;system;;;system;;Operations manager;1313 Willow Rd;Miami;FL;US -17;$2a$10$ItVdBLylXMw3pnv/xkf5qOzrTPos7UngeqVGiP9Z3O7WFBAp7ULry;Olivia;Martinez;olivia.martinez@theflexwork.io;America/New_York;;;USER;Graphic Designer;7.0;True;en;ACT753;RST753;system;;;system;;Graphic designer;1414 Spruce St;Orlando;FL;US -18;$2a$10$ItVdBLylXMw3pnv/xkf5qOzrTPos7UngeqVGiP9Z3O7WFBAp7ULry;Paul;White;paul.white@theflexwork.io;America/Los_Angeles;;;USER;Finance Analyst;;True;en;ACT159;RST159;system;;;system;;Finance analyst;1515 Hickory Dr;San Francisco;CA;US +id;password_hash;first_name;last_name;email;timezone;last_login_time;image_url;role;title;manager_id;activated;lang_key;activation_key;reset_key;created_by;created_at;reset_date;modified_by;modified_at;about;address;city;state;country +1;$2a$10$gSAhZrxMllrbgj/kkK9UceBPpChGWJA7SYIb1Mqo.n5aNLq1/oRrC;John;Doe;admin@theflexwork.io;America/Los_Angeles;;;USER;Software Engineer;;True;en;ACT123;RST123;;;;;;Experienced software engineer;123 Main St;San Francisco;CA;US +2;$2a$10$ItVdBLylXMw3pnv/xkf5qOzrTPos7UngeqVGiP9Z3O7WFBAp7ULry;Jane;Smith;user@theflexwork.io;America/New_York;;;ADMIN;Product Manager;1.0;True;en;ACT456;RST456;;;;;;Product manager with 10 years of experience;456 Oak St;New York;NY;US +3;$2a$10$ItVdBLylXMw3pnv/xkf5qOzrTPos7UngeqVGiP9Z3O7WFBAp7ULry;Alice;Johnson;alice.johnson@theflexwork.io;America/Chicago;;;USER;UX Designer;2.0;False;en;ACT789;RST789;;;;;;Creative UX designer;789 Pine Ave;Chicago;IL;US +4;$2a$10$ItVdBLylXMw3pnv/xkf5qOzrTPos7UngeqVGiP9Z3O7WFBAp7ULry;Bob;Brown;bob.brown@theflexwork.io;America/Los_Angeles;;;MANAGER;Team Lead;1.0;True;en;ACT321;RST321;;;;;;Experienced team leader;101 Maple Dr;Los Angeles;CA;US +5;$2a$10$ItVdBLylXMw3pnv/xkf5qOzrTPos7UngeqVGiP9Z3O7WFBAp7ULry;Charlie;Green;charlie.green@theflexwork.io;America/New_York;;;USER;QA Engineer;2.0;True;en;ACT654;RST654;;;;;;Quality assurance specialist;202 Birch Ln;Boston;MA;US +6;$2a$10$ItVdBLylXMw3pnv/xkf5qOzrTPos7UngeqVGiP9Z3O7WFBAp7ULry;Diana;Prince;diana.prince@theflexwork.io;America/Denver;;;USER;Project Coordinator;3.0;False;en;ACT987;RST987;;;;;;Project coordinator;303 Cedar Rd;Denver;CO;US +7;$2a$10$ItVdBLylXMw3pnv/xkf5qOzrTPos7UngeqVGiP9Z3O7WFBAp7ULry;Edward;Nash;edward.nash@theflexwork.io;America/Los_Angeles;;;USER;Data Analyst;3.0;True;en;ACT741;RST741;;;;;;Data analyst expert;404 Elm St;Seattle;WA;US +8;$2a$10$ItVdBLylXMw3pnv/xkf5qOzrTPos7UngeqVGiP9Z3O7WFBAp7ULry;Fiona;White;fiona.white@theflexwork.io;America/Chicago;;;USER;Marketing Specialist;4.0;True;en;ACT852;RST852;;;;;;Specialist in digital marketing;505 Walnut St;Austin;TX;US +9;$2a$10$ItVdBLylXMw3pnv/xkf5qOzrTPos7UngeqVGiP9Z3O7WFBAp7ULry;George;King;george.king@theflexwork.io;America/New_York;;;ADMIN;IT Manager;;False;en;ACT963;RST963;;;;;;IT manager;606 Cherry Ave;New York;NY;US +10;$2a$10$ItVdBLylXMw3pnv/xkf5qOzrTPos7UngeqVGiP9Z3O7WFBAp7ULry;Hannah;Moore;hannah.moore@theflexwork.io;America/Denver;;;USER;HR Specialist;5.0;True;en;ACT159;RST159;;;;;;Human resources expert;707 Chestnut Dr;Denver;CO;US +11;$2a$10$ItVdBLylXMw3pnv/xkf5qOzrTPos7UngeqVGiP9Z3O7WFBAp7ULry;Isaac;Young;isaac.young@theflexwork.io;America/Chicago;;;USER;Sales Associate;6.0;True;en;ACT753;RST753;;;;;;Sales associate;808 Ash St;Chicago;IL;US +12;$2a$10$ItVdBLylXMw3pnv/xkf5qOzrTPos7UngeqVGiP9Z3O7WFBAp7ULry;Julia;Reed;julia.reed@theflexwork.io;America/Chicago;;;USER;Product Designer;5.0;True;en;ACT369;RST369;;;;;;Product designer;909 Poplar St;Portland;OR;US +13;$2a$10$ItVdBLylXMw3pnv/xkf5qOzrTPos7UngeqVGiP9Z3O7WFBAp7ULry;Kevin;Wright;kevin.wright@theflexwork.io;America/Chicago;;;USER;Software Engineer;5.0;True;en;ACT258;RST258;;;;;;Software engineer;1010 Fir Ave;San Diego;CA;US +14;$2a$10$ItVdBLylXMw3pnv/xkf5qOzrTPos7UngeqVGiP9Z3O7WFBAp7ULry;Liam;Harris;liam.harris@theflexwork.io;America/New_York;;;USER;Support Engineer;6.0;True;en;ACT147;RST147;;;;;;Support engineer;1111 Cypress Dr;Philadelphia;PA;US +15;$2a$10$ItVdBLylXMw3pnv/xkf5qOzrTPos7UngeqVGiP9Z3O7WFBAp7ULry;Mia;Clark;mia.clark@theflexwork.io;America/Los_Angeles;;;USER;Business Analyst;;True;en;ACT369;RST369;;;;;;Business analyst;1212 Redwood Ln;Los Angeles;CA;US +16;$2a$10$ItVdBLylXMw3pnv/xkf5qOzrTPos7UngeqVGiP9Z3O7WFBAp7ULry;Noah;Lewis;noah.lewis@theflexwork.io;America/Denver;;;USER;Operations Manager;7.0;False;en;ACT951;RST951;;;;;;Operations manager;1313 Willow Rd;Miami;FL;US +17;$2a$10$ItVdBLylXMw3pnv/xkf5qOzrTPos7UngeqVGiP9Z3O7WFBAp7ULry;Olivia;Martinez;olivia.martinez@theflexwork.io;America/New_York;;;USER;Graphic Designer;7.0;True;en;ACT753;RST753;;;;;;Graphic designer;1414 Spruce St;Orlando;FL;US +18;$2a$10$ItVdBLylXMw3pnv/xkf5qOzrTPos7UngeqVGiP9Z3O7WFBAp7ULry;Paul;White;paul.white@theflexwork.io;America/Los_Angeles;;;USER;Finance Analyst;;True;en;ACT159;RST159;;;;;;Finance analyst;1515 Hickory Dr;San Francisco;CA;US