diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c267aa57..a8244e11 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,6 @@ [versions] mapstructVersion = "1.6.2" archunitJunit5Version = "1.3.0" -springStateMachineVersion = "4.0.0" lombokVersion = "1.18.34" liquibaseVersion = "4.30.0" assertJVersion = "3.26.3" @@ -22,9 +21,6 @@ shedlockVersion="6.0.1" [libraries] jhipster-framework = { module = "tech.jhipster:jhipster-framework", version.ref = "jhisterVersion" } lombok = { module = "org.projectlombok:lombok", version.ref = "lombokVersion" } -spring-statemachine-starter = { module = "org.springframework.statemachine:spring-statemachine-starter", version.ref = "springStateMachineVersion" } -spring-statemachine-jpa = { module = "org.springframework.statemachine:spring-statemachine-data-jpa", version.ref = "springStateMachineVersion" } -spring-statemachine-test = { module = "org.springframework.statemachine:spring-statemachine-test", version.ref = "springStateMachineVersion" } liquibase = { module = "org.liquibase:liquibase-core", version.ref = "liquibaseVersion" } assertJ = { module = "org.assertj:assertj-core", version.ref = "assertJVersion" } mapstruct = { module = "org.mapstruct:mapstruct", version.ref = "mapstructVersion" } @@ -48,7 +44,6 @@ spring-boot = { id = "org.springframework.boot", version.ref = "springbootVersio spring-dependency-management = { id = "io.spring.dependency-management", version.ref="springDependencyManagementVersion" } [bundles] -spring-statemachine = ["spring-statemachine-starter", "spring-statemachine-jpa"] logback = ["logback-classic"] junit = ["junit-jupiter-api", "junit-jupiter-engine"] mockito = ["mockito", "mockito-junit"] diff --git a/server/build.gradle b/server/build.gradle index d7a63c65..ca644720 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -118,7 +118,6 @@ dependencies { implementation("org.hibernate.orm:hibernate-core") implementation("org.hibernate.validator:hibernate-validator") - implementation(libs.bundles.spring.statemachine) implementation(libs.liquibase) implementation(libs.bundles.shedlock) @@ -135,7 +134,6 @@ dependencies { testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.springframework.boot:spring-boot-test") testImplementation("org.springframework.security:spring-security-test") - testImplementation(libs.spring.statemachine.test) testCompileOnly(libs.lombok) testAnnotationProcessor(libs.lombok) testImplementation("org.jetbrains.kotlin:kotlin-test") 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 7aa6f358..96093173 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 @@ -5,8 +5,6 @@ import jakarta.persistence.Column; import jakarta.persistence.Convert; import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; @@ -52,8 +50,8 @@ public class TeamRequest extends AbstractAuditingEntity { private String requestDescription; - @Column(nullable = false, length = 50) - @Enumerated(EnumType.STRING) + @Column(nullable = false) + @Convert(converter = TeamRequestPriorityConverter.class) private TeamRequestPriority priority; @Column(name = "is_deleted", nullable = false) diff --git a/server/src/main/java/io/flexwork/modules/teams/domain/TeamRequestPriority.java b/server/src/main/java/io/flexwork/modules/teams/domain/TeamRequestPriority.java index 691eebbe..f1c8b60d 100644 --- a/server/src/main/java/io/flexwork/modules/teams/domain/TeamRequestPriority.java +++ b/server/src/main/java/io/flexwork/modules/teams/domain/TeamRequestPriority.java @@ -1,9 +1,28 @@ package io.flexwork.modules.teams.domain; public enum TeamRequestPriority { - Critical, - High, - Medium, - Low, - Trivial + Critical(0), + High(1), + Medium(2), + Low(3), + Trivial(4); + + private final int code; + + TeamRequestPriority(int code) { + this.code = code; + } + + public int getCode() { + return code; + } + + public static TeamRequestPriority fromCode(int code) { + for (TeamRequestPriority priority : TeamRequestPriority.values()) { + if (priority.code == code) { + return priority; + } + } + throw new IllegalArgumentException("Invalid code for TeamRequestPriority: " + code); + } } diff --git a/server/src/main/java/io/flexwork/modules/teams/domain/TeamRequestPriorityConverter.java b/server/src/main/java/io/flexwork/modules/teams/domain/TeamRequestPriorityConverter.java new file mode 100644 index 00000000..ed127579 --- /dev/null +++ b/server/src/main/java/io/flexwork/modules/teams/domain/TeamRequestPriorityConverter.java @@ -0,0 +1,19 @@ +package io.flexwork.modules.teams.domain; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +@Converter(autoApply = true) +public class TeamRequestPriorityConverter + implements AttributeConverter { + + @Override + public Integer convertToDatabaseColumn(TeamRequestPriority priority) { + return (priority != null) ? priority.getCode() : null; + } + + @Override + public TeamRequestPriority convertToEntityAttribute(Integer code) { + return (code != null) ? TeamRequestPriority.fromCode(code) : null; + } +} diff --git a/server/src/main/java/io/flexwork/modules/teams/domain/WorkflowTransitionHistory.java b/server/src/main/java/io/flexwork/modules/teams/domain/WorkflowTransitionHistory.java index b793527d..620f0fd6 100644 --- a/server/src/main/java/io/flexwork/modules/teams/domain/WorkflowTransitionHistory.java +++ b/server/src/main/java/io/flexwork/modules/teams/domain/WorkflowTransitionHistory.java @@ -2,6 +2,8 @@ import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @@ -48,5 +50,6 @@ public class WorkflowTransitionHistory { private ZonedDateTime slaDueDate; @Column(name = "status") - private String status; + @Enumerated(EnumType.STRING) + private WorkflowTransitionHistoryStatus status; } diff --git a/server/src/main/java/io/flexwork/modules/teams/domain/WorkflowTransitionHistoryStatus.java b/server/src/main/java/io/flexwork/modules/teams/domain/WorkflowTransitionHistoryStatus.java new file mode 100644 index 00000000..d9cf56e9 --- /dev/null +++ b/server/src/main/java/io/flexwork/modules/teams/domain/WorkflowTransitionHistoryStatus.java @@ -0,0 +1,7 @@ +package io.flexwork.modules.teams.domain; + +public enum WorkflowTransitionHistoryStatus { + In_Progress, + Overdue, + Completed +} 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 0c115800..c385e329 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,8 +1,12 @@ package io.flexwork.modules.teams.repository; import io.flexwork.modules.teams.domain.TeamRequest; +import io.flexwork.modules.teams.domain.WorkflowTransitionHistoryStatus; import io.flexwork.modules.teams.service.dto.PriorityDistributionDTO; +import io.flexwork.modules.teams.service.dto.TicketActionCountByDateDTO; import io.flexwork.modules.teams.service.dto.TicketDistributionDTO; +import io.flexwork.modules.usermanagement.service.dto.TicketStatisticsDTO; +import java.time.Instant; import java.util.List; import java.util.Optional; import org.springframework.data.domain.Page; @@ -80,29 +84,8 @@ public interface TeamRequestRepository @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 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 findUnassignedTicketsByTeamIdDesc( + + "AND r.assignUser IS NULL") + Page findUnassignedTicketsByTeamId( @Param("teamId") Long teamId, Pageable pageable); // Query to count tickets by priority for a specific team @@ -114,4 +97,64 @@ Page findUnassignedTicketsByTeamIdDesc( + "GROUP BY r.priority") List findTicketPriorityDistributionByTeamId( @Param("teamId") Long teamId); + + @Query( + "SELECT new io.flexwork.modules.usermanagement.service.dto.TicketStatisticsDTO(" + + "COUNT(tr), " + + "SUM(CASE WHEN tr.isCompleted = false THEN 1 ELSE 0 END), " + + "SUM(CASE WHEN tr.isCompleted = true THEN 1 ELSE 0 END)) " + + "FROM TeamRequest tr " + + "WHERE tr.isDeleted = false AND tr.team.id = :teamId") + TicketStatisticsDTO getTicketStatisticsByTeamId(@Param("teamId") Long teamId); + + @Query( + "SELECT r " + + "FROM TeamRequest r " + + "JOIN WorkflowTransitionHistory h ON h.teamRequest.id = r.id " + + "WHERE r.isDeleted = false " + + "AND r.isCompleted = false " + + "AND h.slaDueDate IS NOT NULL " + + "AND h.slaDueDate < CURRENT_TIMESTAMP " + + "AND h.status <> :status " + + "AND r.team.id = :teamId") + Page findOverdueTicketsByTeamId( + @Param("teamId") Long teamId, + @Param("status") WorkflowTransitionHistoryStatus completedStatus, + Pageable pageable); + + @Query( + "SELECT COUNT(r.id) " + + "FROM TeamRequest r " + + "JOIN WorkflowTransitionHistory h ON h.teamRequest.id = r.id " + + "WHERE r.isDeleted = false " + + "AND r.isCompleted = false " + + "AND h.slaDueDate IS NOT NULL " + + "AND h.slaDueDate < CURRENT_TIMESTAMP " + + "AND h.status <> :status " + + "AND r.team.id = :teamId") + Long countOverdueTicketsByTeamId( + @Param("teamId") Long teamId, + @Param("status") WorkflowTransitionHistoryStatus completedStatus); + + @Query( + "SELECT new io.flexwork.modules.teams.service.dto.TicketActionCountByDateDTO(" + + "CAST(r.createdAt AS date), " + + "COUNT(r.id), " + + "COALESCE(closedTicketCounts.closedCount, 0)) " + + "FROM TeamRequest r " + + "LEFT JOIN (" + + " SELECT CAST(c.actualCompletionDate AS date) AS completionDate, COUNT(c.id) AS closedCount " + + " FROM TeamRequest c " + + " WHERE c.isDeleted = false " + + " AND c.team.id = :teamId " + + " AND c.actualCompletionDate IS NOT NULL " + + " GROUP BY CAST(c.actualCompletionDate AS date)" + + ") closedTicketCounts ON CAST(r.createdAt AS date) = closedTicketCounts.completionDate " + + "WHERE r.isDeleted = false " + + "AND r.team.id = :teamId " + + "AND r.createdAt >= :startDate " + + "GROUP BY CAST(r.createdAt AS date), closedTicketCounts.closedCount " + + "ORDER BY CAST(r.createdAt AS date) ASC") + List findTicketActionByDaySeries( + @Param("teamId") Long teamId, @Param("startDate") Instant startDate); } diff --git a/server/src/main/java/io/flexwork/modules/teams/repository/WorkflowTransitionRepository.java b/server/src/main/java/io/flexwork/modules/teams/repository/WorkflowTransitionRepository.java index 489bad6d..5e7fc220 100644 --- a/server/src/main/java/io/flexwork/modules/teams/repository/WorkflowTransitionRepository.java +++ b/server/src/main/java/io/flexwork/modules/teams/repository/WorkflowTransitionRepository.java @@ -25,6 +25,25 @@ public interface WorkflowTransitionRepository extends JpaRepository findValidTargetStates( @Param("workflowId") Long workflowId, @Param("sourceStateId") Long sourceStateId); + /** + * @param workflowId + * @param sourceStateId + * @return + */ + @Query( + "SELECT wt " + + "FROM WorkflowTransition wt " + + "WHERE wt.sourceState.id = :sourceStateId " + + "AND wt.workflow.id = :workflowId") + List findTransitionsBySourceState( + @Param("workflowId") Long workflowId, @Param("sourceStateId") Long sourceStateId); + + /** + * @param workflowId + * @param sourceStateId + * @param targetStateId + * @return + */ Optional findByWorkflowIdAndSourceStateIdAndTargetStateId( Long workflowId, Long sourceStateId, Long targetStateId); } 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 ed69d1ab..7626c4ff 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 @@ -1,25 +1,39 @@ package io.flexwork.modules.teams.service; +import static io.flexwork.modules.teams.domain.WorkflowTransitionHistoryStatus.Completed; import static io.flexwork.query.QueryUtils.createSpecification; import io.flexwork.modules.audit.AuditLogUpdateEvent; import io.flexwork.modules.teams.domain.TeamRequest; import io.flexwork.modules.teams.domain.WorkflowState; +import io.flexwork.modules.teams.domain.WorkflowTransition; +import io.flexwork.modules.teams.domain.WorkflowTransitionHistory; +import io.flexwork.modules.teams.domain.WorkflowTransitionHistoryStatus; 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.repository.WorkflowTransitionHistoryRepository; +import io.flexwork.modules.teams.repository.WorkflowTransitionRepository; import io.flexwork.modules.teams.service.dto.PriorityDistributionDTO; import io.flexwork.modules.teams.service.dto.TeamRequestDTO; +import io.flexwork.modules.teams.service.dto.TicketActionCountByDateDTO; import io.flexwork.modules.teams.service.dto.TicketDistributionDTO; import io.flexwork.modules.teams.service.event.NewTeamRequestCreatedEvent; import io.flexwork.modules.teams.service.event.TeamRequestWorkStateTransitionEvent; import io.flexwork.modules.teams.service.mapper.TeamRequestMapper; +import io.flexwork.modules.usermanagement.service.dto.TicketStatisticsDTO; import io.flexwork.query.GroupFilter; import io.flexwork.query.QueryDTO; import jakarta.persistence.EntityManager; import jakarta.persistence.EntityNotFoundException; import jakarta.persistence.PersistenceContext; +import java.time.LocalDate; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import org.jclouds.rest.ResourceNotFoundException; @@ -39,20 +53,23 @@ public class TeamRequestService { private final TeamRequestRepository teamRequestRepository; private final TeamRequestMapper teamRequestMapper; private final WorkflowStateRepository workflowStateRepository; - private final WorkflowRepository workflowRepository; + private final WorkflowTransitionRepository workflowTransitionRepository; + private final WorkflowTransitionHistoryRepository workflowTransitionHistoryRepository; private final ApplicationEventPublisher eventPublisher; @Autowired public TeamRequestService( TeamRequestRepository teamRequestRepository, TeamRequestMapper teamRequestMapper, - WorkflowRepository workflowRepository, + WorkflowTransitionRepository workflowTransitionRepository, WorkflowStateRepository workflowStateRepository, + WorkflowTransitionHistoryRepository workflowTransitionHistoryRepository, ApplicationEventPublisher eventPublisher) { this.teamRequestRepository = teamRequestRepository; this.teamRequestMapper = teamRequestMapper; - this.workflowRepository = workflowRepository; + this.workflowTransitionRepository = workflowTransitionRepository; this.workflowStateRepository = workflowStateRepository; + this.workflowTransitionHistoryRepository = workflowTransitionHistoryRepository; this.eventPublisher = eventPublisher; } @@ -98,6 +115,20 @@ public TeamRequestDTO createTeamRequest(TeamRequestDTO teamRequestDTO) { // Clear the persistence context to force a reload entityManager.clear(); + ZonedDateTime slaDueDate = + calculateEarliestSlaDueDate( + teamRequest.getWorkflow().getId(), initialStateByWorkflowId.getId()); + + WorkflowTransitionHistory history = new WorkflowTransitionHistory(); + history.setTeamRequest(teamRequest); + history.setFromState(null); + history.setToState(initialStateByWorkflowId); + history.setEventName("Created"); + history.setTransitionDate(ZonedDateTime.now()); + history.setSlaDueDate(slaDueDate); + history.setStatus(WorkflowTransitionHistoryStatus.In_Progress); + workflowTransitionHistoryRepository.save(history); + teamRequest = teamRequestRepository .findById(teamRequest.getId()) @@ -123,13 +154,17 @@ public TeamRequestDTO updateTeamRequest(TeamRequestDTO teamRequestDTO) { teamRequestMapper.updateEntity(teamRequestDTO, existingTeamRequest); - boolean isStateChanged = (previousState != teamRequestDTO.getCurrentStateId()); + boolean isStateChanged = + (!Objects.equals(previousState, teamRequestDTO.getCurrentStateId())); existingTeamRequest.setIsNew(!isStateChanged); if (isStateChanged) { boolean finalState = workflowStateRepository.isFinalState( teamRequestDTO.getWorkflowId(), teamRequestDTO.getCurrentStateId()); existingTeamRequest.setIsCompleted(finalState); + if (teamRequestDTO.getActualCompletionDate() == null) { + existingTeamRequest.setActualCompletionDate(LocalDate.now()); + } } TeamRequestDTO savedTeamRequest = @@ -138,7 +173,7 @@ public TeamRequestDTO updateTeamRequest(TeamRequestDTO teamRequestDTO) { new AuditLogUpdateEvent(this, previousTeamRequest, teamRequestDTO)); Long currentState = savedTeamRequest.getCurrentStateId(); - if (previousState != currentState) { + if (!Objects.equals(previousState, currentState)) { eventPublisher.publishEvent( new TeamRequestWorkStateTransitionEvent( this, teamRequestDTO.getId(), previousState, currentState)); @@ -213,15 +248,9 @@ public List getTicketDistribution(Long teamId) { } // Fetch unassigned tickets - public Page getUnassignedTickets( - Long teamId, String sortDirection, Pageable pageable) { - if ("desc".equalsIgnoreCase(sortDirection)) { - return teamRequestRepository - .findUnassignedTicketsByTeamIdDesc(teamId, pageable) - .map(teamRequestMapper::toDto); - } + public Page getUnassignedTickets(Long teamId, Pageable pageable) { return teamRequestRepository - .findUnassignedTicketsByTeamIdAsc(teamId, pageable) + .findUnassignedTicketsByTeamId(teamId, pageable) .map(teamRequestMapper::toDto); } @@ -229,4 +258,76 @@ public Page getUnassignedTickets( public List getPriorityDistribution(Long teamId) { return teamRequestRepository.findTicketPriorityDistributionByTeamId(teamId); } + + public TicketStatisticsDTO getTicketStatisticsByTeamId(Long teamId) { + return teamRequestRepository.getTicketStatisticsByTeamId(teamId); + } + + private ZonedDateTime calculateEarliestSlaDueDate(Long workflowId, Long sourceStateId) { + // Fetch all transitions from the current state + List transitions = + workflowTransitionRepository.findTransitionsBySourceState( + workflowId, sourceStateId); + + if (transitions.isEmpty()) { + throw new IllegalStateException("No transitions defined for the current state."); + } + + // Find the transition with the lowest SLA duration + WorkflowTransition earliestTransition = + transitions.stream() + .filter( + t -> + t.getSlaDuration() + != null) // Exclude transitions without SLA duration + .min( + Comparator.comparing( + WorkflowTransition + ::getSlaDuration)) // Find the minimum SLA duration + .orElse(null); + + if (earliestTransition == null) { + return null; // No SLA defined for any transition + } + + // Calculate the SLA due date for the earliest transition + return ZonedDateTime.now().plusMinutes(earliestTransition.getSlaDuration()); + } + + public Page getOverdueTickets(Long teamId, Pageable pageable) { + return teamRequestRepository + .findOverdueTicketsByTeamId(teamId, Completed, pageable) + .map(teamRequestMapper::toDto); + } + + public Long countOverdueTickets(Long teamId) { + return teamRequestRepository.countOverdueTicketsByTeamId(teamId, Completed); + } + + public List getTicketCreationTimeseries(Long teamId, int days) { + if (days <= 0) { + days = 7; // Default to 7 days + } + + LocalDate startDate = LocalDate.now().minusDays(days - 1); + List trends = + teamRequestRepository.findTicketActionByDaySeries( + teamId, startDate.atStartOfDay().toInstant(ZoneOffset.UTC)); + + // Fill missing dates with zero counts + Map trendMap = new HashMap<>(); + for (TicketActionCountByDateDTO trend : trends) { + trendMap.put(trend.getDate(), trend); + } + + List ticketByDaySeries = new ArrayList<>(); + for (int i = 0; i < days; i++) { + LocalDate date = startDate.plusDays(i); + TicketActionCountByDateDTO trend = + trendMap.getOrDefault(date, new TicketActionCountByDateDTO(date, 0L, 0L)); + ticketByDaySeries.add(trend); + } + + return ticketByDaySeries; + } } diff --git a/server/src/main/java/io/flexwork/modules/teams/service/WorkflowTransitionHistoryService.java b/server/src/main/java/io/flexwork/modules/teams/service/WorkflowTransitionHistoryService.java index e6062e6c..04c616f9 100644 --- a/server/src/main/java/io/flexwork/modules/teams/service/WorkflowTransitionHistoryService.java +++ b/server/src/main/java/io/flexwork/modules/teams/service/WorkflowTransitionHistoryService.java @@ -1,5 +1,8 @@ package io.flexwork.modules.teams.service; +import static io.flexwork.modules.teams.domain.WorkflowTransitionHistoryStatus.Completed; +import static io.flexwork.modules.teams.domain.WorkflowTransitionHistoryStatus.In_Progress; + import io.flexwork.modules.teams.domain.TeamRequest; import io.flexwork.modules.teams.domain.WorkflowTransition; import io.flexwork.modules.teams.domain.WorkflowTransitionHistory; @@ -40,11 +43,10 @@ public WorkflowTransitionHistoryService( * @param teamRequestId The ID of the team request * @param fromStateId The ID of the source state * @param toStateId The ID of the target state - * @param status The status of the transition (optional) */ @Transactional public void recordWorkflowTransitionHistory( - Long teamRequestId, Long fromStateId, Long toStateId, String status) { + Long teamRequestId, Long fromStateId, Long toStateId) { // Fetch the associated entities TeamRequest teamRequest = teamRequestRepository @@ -78,7 +80,11 @@ public void recordWorkflowTransitionHistory( history.setEventName(transition.getEventName()); history.setTransitionDate(ZonedDateTime.now(ZoneId.of("UTC"))); history.setSlaDueDate(slaDueDate); - history.setStatus(status != null ? status : "PENDING"); + if (transition.getTargetState().getIsFinal()) { + history.setStatus(Completed); + } else { + history.setStatus(In_Progress); + } workflowTransitionHistoryRepository.save(history); } 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 deleted file mode 100644 index a21a9357..00000000 --- a/server/src/main/java/io/flexwork/modules/teams/service/dto/SlaDurationDTO.java +++ /dev/null @@ -1,15 +0,0 @@ -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/TicketActionCountByDateDTO.java b/server/src/main/java/io/flexwork/modules/teams/service/dto/TicketActionCountByDateDTO.java new file mode 100644 index 00000000..4eb8883b --- /dev/null +++ b/server/src/main/java/io/flexwork/modules/teams/service/dto/TicketActionCountByDateDTO.java @@ -0,0 +1,23 @@ +package io.flexwork.modules.teams.service.dto; + +import java.sql.Date; +import java.time.LocalDate; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class TicketActionCountByDateDTO { + private LocalDate date; + private Long createdCount; + private Long closedCount; + + public TicketActionCountByDateDTO(Date creationInstant, Long createdCount, Long closedCount) { + // Convert Instant to LocalDate + this.date = creationInstant.toLocalDate(); + this.createdCount = createdCount; + this.closedCount = closedCount; + } +} diff --git a/server/src/main/java/io/flexwork/modules/teams/service/listener/TeamRequestWorkStateTransitionEventListener.java b/server/src/main/java/io/flexwork/modules/teams/service/listener/TeamRequestWorkStateTransitionEventListener.java index 55c04ca6..b063ab67 100644 --- a/server/src/main/java/io/flexwork/modules/teams/service/listener/TeamRequestWorkStateTransitionEventListener.java +++ b/server/src/main/java/io/flexwork/modules/teams/service/listener/TeamRequestWorkStateTransitionEventListener.java @@ -22,9 +22,6 @@ public TeamRequestWorkStateTransitionEventListener( @Transactional public void onWorkflowStateTransition(TeamRequestWorkStateTransitionEvent event) { workflowTransitionHistoryService.recordWorkflowTransitionHistory( - event.getTeamRequestId(), - event.getSourceStateId(), - event.getTargetStateId(), - "Completed"); + event.getTeamRequestId(), event.getSourceStateId(), event.getTargetStateId()); } } 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 56b491b5..40149f5a 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 @@ -4,13 +4,14 @@ import io.flexwork.modules.teams.service.WorkflowTransitionHistoryService; import io.flexwork.modules.teams.service.dto.PriorityDistributionDTO; import io.flexwork.modules.teams.service.dto.TeamRequestDTO; +import io.flexwork.modules.teams.service.dto.TicketActionCountByDateDTO; import io.flexwork.modules.teams.service.dto.TicketDistributionDTO; import io.flexwork.modules.teams.service.dto.TransitionItemCollectionDTO; +import io.flexwork.modules.usermanagement.service.dto.TicketStatisticsDTO; 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; @@ -96,15 +97,8 @@ public List getTicketDistribution(@PathVariable Long team // Endpoint to get unassigned tickets for a specific team @GetMapping("/{teamId}/unassigned-tickets") - public Page 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); + public Page getUnassignedTickets(@PathVariable Long teamId, Pageable pageable) { + return teamRequestService.getUnassignedTickets(teamId, pageable); } // Endpoint to get priority distribution for a specific team @@ -127,4 +121,26 @@ public ResponseEntity getTicketStateChangesHistory( return ResponseEntity.ok(ticketHistory); } + + @GetMapping("/{teamId}/statistics") + public TicketStatisticsDTO getTicketStatisticsByTeamId(@PathVariable Long teamId) { + return teamRequestService.getTicketStatisticsByTeamId(teamId); + } + + @GetMapping("/{teamId}/overdue-tickets") + public Page getOverdueTickets(@PathVariable Long teamId, Pageable pageable) { + return teamRequestService.getOverdueTickets(teamId, pageable); + } + + @GetMapping("/{teamId}/overdue-tickets/count") + public Long countOverdueTickets(@PathVariable Long teamId) { + return teamRequestService.countOverdueTickets(teamId); + } + + @GetMapping("/{teamId}/ticket-creations-day-series") + public List getTicketCreationDaySeries( + @PathVariable Long teamId, + @RequestParam(required = false, defaultValue = "7") int days) { + return teamRequestService.getTicketCreationTimeseries(teamId, days); + } } diff --git a/server/src/main/java/io/flexwork/modules/usermanagement/service/SignupService.java b/server/src/main/java/io/flexwork/modules/usermanagement/service/SignupService.java deleted file mode 100644 index 4c335e43..00000000 --- a/server/src/main/java/io/flexwork/modules/usermanagement/service/SignupService.java +++ /dev/null @@ -1,64 +0,0 @@ -package io.flexwork.modules.usermanagement.service; - -import io.flexwork.modules.usermanagement.domain.User; -import io.flexwork.modules.usermanagement.repository.UserRepository; -import io.flexwork.modules.usermanagement.stateMachine.SignupEvents; -import io.flexwork.modules.usermanagement.stateMachine.SignupStates; -import java.util.Optional; -import lombok.SneakyThrows; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.messaging.support.MessageBuilder; -import org.springframework.statemachine.StateMachine; -import org.springframework.statemachine.annotation.WithStateMachine; -import org.springframework.statemachine.persist.StateMachinePersister; -import org.springframework.statemachine.service.StateMachineService; -import org.springframework.stereotype.Service; -import reactor.core.publisher.Mono; - -@Service -@WithStateMachine -public class SignupService { - - private static final Logger LOG = LoggerFactory.getLogger(SignupService.class); - - private final UserRepository userRepository; - - private final StateMachineService stateMachineService; - - private final StateMachinePersister stateMachinePersister; - - public SignupService( - UserRepository userRepository, - StateMachineService stateMachineService, - StateMachinePersister stateMachinePersister) { - this.userRepository = userRepository; - this.stateMachineService = stateMachineService; - this.stateMachinePersister = stateMachinePersister; - } - - @SneakyThrows - public void signup(User user) { - LOG.debug("Start signup workflow {}", user); - Optional existingUser = userRepository.findById(user.getId()); - if (existingUser.isPresent()) { - throw new IllegalArgumentException("User " + user.getId() + " existed"); - } else { - userRepository.save(user); - StateMachine stateMachine = - stateMachineService.acquireStateMachine("signup-" + user.getId(), true); - - stateMachine.getExtendedState().getVariables().put("user", user); - stateMachine - .sendEvent( - Mono.just(MessageBuilder.withPayload(SignupEvents.NEW_SIGNUP).build())) - .subscribe( - signupStatesSignupEventsStateMachineEventResult -> - LOG.debug( - "Result {}", - signupStatesSignupEventsStateMachineEventResult - .getResultType())); - stateMachinePersister.persist(stateMachine, "signup-" + user.getId()); - } - } -} diff --git a/server/src/main/java/io/flexwork/modules/usermanagement/service/dto/TicketStatisticsDTO.java b/server/src/main/java/io/flexwork/modules/usermanagement/service/dto/TicketStatisticsDTO.java new file mode 100644 index 00000000..e3ac080d --- /dev/null +++ b/server/src/main/java/io/flexwork/modules/usermanagement/service/dto/TicketStatisticsDTO.java @@ -0,0 +1,14 @@ +package io.flexwork.modules.usermanagement.service.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class TicketStatisticsDTO { + private Long totalTickets; + private Long pendingTickets; + private Long completedTickets; +} diff --git a/server/src/main/java/io/flexwork/modules/usermanagement/stateMachine/SignupEvents.java b/server/src/main/java/io/flexwork/modules/usermanagement/stateMachine/SignupEvents.java deleted file mode 100644 index 6c54475b..00000000 --- a/server/src/main/java/io/flexwork/modules/usermanagement/stateMachine/SignupEvents.java +++ /dev/null @@ -1,8 +0,0 @@ -package io.flexwork.modules.usermanagement.stateMachine; - -public enum SignupEvents { - NEW_SIGNUP, - SIGNUP_VERIFICATION, - SIGNUP_FAILED, - SIGNUP_SUCCESS, -} diff --git a/server/src/main/java/io/flexwork/modules/usermanagement/stateMachine/SignupStates.java b/server/src/main/java/io/flexwork/modules/usermanagement/stateMachine/SignupStates.java deleted file mode 100644 index 99ed043b..00000000 --- a/server/src/main/java/io/flexwork/modules/usermanagement/stateMachine/SignupStates.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.flexwork.modules.usermanagement.stateMachine; - -public enum SignupStates { - NEW_SIGNUP_USER, - SIGNING_UP, - SIGNUP_VERIFICATION, - SIGNUP_VERIFICATION_FAILED, - SIGNUP_VERIFICATION_SUCCESS, - SIGNUP_COMPLETED, - ACTIVE -} diff --git a/server/src/main/java/io/flexwork/modules/usermanagement/stateMachine/actions/NewSignUpAction.java b/server/src/main/java/io/flexwork/modules/usermanagement/stateMachine/actions/NewSignUpAction.java deleted file mode 100644 index d37e080e..00000000 --- a/server/src/main/java/io/flexwork/modules/usermanagement/stateMachine/actions/NewSignUpAction.java +++ /dev/null @@ -1,45 +0,0 @@ -package io.flexwork.modules.usermanagement.stateMachine.actions; - -import io.flexwork.modules.usermanagement.domain.User; -import io.flexwork.modules.usermanagement.stateMachine.SignupEvents; -import io.flexwork.modules.usermanagement.stateMachine.SignupStates; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.mail.SimpleMailMessage; -import org.springframework.mail.javamail.JavaMailSender; -import org.springframework.statemachine.StateContext; -import org.springframework.statemachine.action.Action; -import org.springframework.stereotype.Component; -import org.thymeleaf.context.Context; -import org.thymeleaf.spring6.SpringTemplateEngine; - -@Component -public class NewSignUpAction implements Action { - - private static final Logger LOG = LoggerFactory.getLogger(NewSignUpAction.class); - - private final JavaMailSender mailSender; - - private final SpringTemplateEngine templateEngine; - - public NewSignUpAction(JavaMailSender mailSender, SpringTemplateEngine templateEngine) { - this.mailSender = mailSender; - this.templateEngine = templateEngine; - } - - @Override - public void execute(StateContext context) { - User user = (User) context.getExtendedState().getVariables().get("user"); - - LOG.debug("Send email to the new user"); - SimpleMailMessage message = new SimpleMailMessage(); - message.setTo("haiphucnguyen@gmail.com"); - message.setSubject("Sign up"); - message.setFrom("noreply@flexwork.com"); - Context templateContext = new Context(); - templateContext.setVariable("user", user); - String body = templateEngine.process("email_verification.html", templateContext); - message.setText(body); - mailSender.send(message); - } -} diff --git a/server/src/main/java/io/flexwork/modules/usermanagement/stateMachine/actions/NewSignupVerificationAction.java b/server/src/main/java/io/flexwork/modules/usermanagement/stateMachine/actions/NewSignupVerificationAction.java deleted file mode 100644 index efd6119c..00000000 --- a/server/src/main/java/io/flexwork/modules/usermanagement/stateMachine/actions/NewSignupVerificationAction.java +++ /dev/null @@ -1,20 +0,0 @@ -package io.flexwork.modules.usermanagement.stateMachine.actions; - -import io.flexwork.modules.usermanagement.stateMachine.SignupEvents; -import io.flexwork.modules.usermanagement.stateMachine.SignupStates; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.statemachine.StateContext; -import org.springframework.statemachine.action.Action; -import org.springframework.stereotype.Component; - -@Component -public class NewSignupVerificationAction implements Action { - - private static final Logger LOG = LoggerFactory.getLogger(NewSignupVerificationAction.class); - - @Override - public void execute(StateContext context) { - LOG.debug("Verifying the new signup user"); - } -} diff --git a/server/src/main/java/io/flexwork/modules/usermanagement/stateMachine/config/StateMachineSignupConfigurer.java b/server/src/main/java/io/flexwork/modules/usermanagement/stateMachine/config/StateMachineSignupConfigurer.java deleted file mode 100644 index a4e2aa34..00000000 --- a/server/src/main/java/io/flexwork/modules/usermanagement/stateMachine/config/StateMachineSignupConfigurer.java +++ /dev/null @@ -1,109 +0,0 @@ -package io.flexwork.modules.usermanagement.stateMachine.config; - -import io.flexwork.modules.usermanagement.stateMachine.SignupEvents; -import io.flexwork.modules.usermanagement.stateMachine.SignupStates; -import io.flexwork.modules.usermanagement.stateMachine.actions.NewSignUpAction; -import io.flexwork.modules.usermanagement.stateMachine.actions.NewSignupVerificationAction; -import io.flexwork.statemachine.support.ErrorHandlingAction; -import java.util.EnumSet; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.statemachine.config.EnableStateMachineFactory; -import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter; -import org.springframework.statemachine.config.StateMachineFactory; -import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer; -import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; -import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; -import org.springframework.statemachine.data.jpa.JpaPersistingStateMachineInterceptor; -import org.springframework.statemachine.data.jpa.JpaStateMachineRepository; -import org.springframework.statemachine.persist.DefaultStateMachinePersister; -import org.springframework.statemachine.persist.StateMachineRuntimePersister; -import org.springframework.statemachine.service.DefaultStateMachineService; -import org.springframework.statemachine.service.StateMachineService; - -@Configuration -public class StateMachineSignupConfigurer { - - @Configuration - public static class StateMachinePersisterConfig { - - @Bean - public StateMachineRuntimePersister stateMachinePersist( - JpaStateMachineRepository jpaStateMachineRepository) { - return new JpaPersistingStateMachineInterceptor<>(jpaStateMachineRepository); - } - - @Bean - public DefaultStateMachinePersister - stateMachinePersister(JpaStateMachineRepository jpaStateMachineRepository) { - return new DefaultStateMachinePersister<>( - new JpaPersistingStateMachineInterceptor<>(jpaStateMachineRepository)); - } - } - - @Configuration - @EnableStateMachineFactory - public static class StateMachineConfig - extends EnumStateMachineConfigurerAdapter { - - @Autowired private NewSignUpAction newSignUpAction; - - @Autowired private NewSignupVerificationAction newSignupVerificationAction; - - private StateMachineRuntimePersister - stateMachineRuntimePersister; - - @Override - public void configure( - StateMachineConfigurationConfigurer config) - throws Exception { - config.withPersistence().runtimePersister(stateMachineRuntimePersister); - } - - @Override - public void configure(StateMachineStateConfigurer states) - throws Exception { - states.withStates() - .initial(SignupStates.NEW_SIGNUP_USER) - .states(EnumSet.allOf(SignupStates.class)) - .end(SignupStates.SIGNUP_COMPLETED); - } - - @Override - public void configure( - StateMachineTransitionConfigurer transitions) - throws Exception { - transitions - .withExternal() - .source(SignupStates.NEW_SIGNUP_USER) - .target(SignupStates.SIGNING_UP) - .event(SignupEvents.NEW_SIGNUP) - .action(newSignUpAction, new ErrorHandlingAction<>()) - .and() - .withExternal() - .source(SignupStates.SIGNING_UP) - .target(SignupStates.SIGNUP_VERIFICATION) - .event(SignupEvents.SIGNUP_VERIFICATION) - .action(newSignupVerificationAction) - .and() - .withExternal() - .source(SignupStates.SIGNUP_VERIFICATION) - .target(SignupStates.SIGNUP_COMPLETED) - .event(SignupEvents.SIGNUP_SUCCESS); - } - } - - @Configuration - public static class ServiceConfig { - - @Bean - public StateMachineService stateMachineService( - StateMachineFactory stateMachineFactory, - StateMachineRuntimePersister - stateMachineRuntimePersister) { - return new DefaultStateMachineService<>( - stateMachineFactory, stateMachineRuntimePersister); - } - } -} diff --git a/server/src/main/java/io/flexwork/modules/usermanagement/web/rest/SignupController.java b/server/src/main/java/io/flexwork/modules/usermanagement/web/rest/SignupController.java deleted file mode 100644 index 92f7bb68..00000000 --- a/server/src/main/java/io/flexwork/modules/usermanagement/web/rest/SignupController.java +++ /dev/null @@ -1,25 +0,0 @@ -package io.flexwork.modules.usermanagement.web.rest; - -import io.flexwork.modules.usermanagement.domain.User; -import io.flexwork.modules.usermanagement.service.SignupService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.web.bind.annotation.*; - -@RestController -@RequestMapping("/api/auth") -public class SignupController { - - private static final Logger LOG = LoggerFactory.getLogger(SignupController.class); - - private SignupService signupService; - - public SignupController(SignupService signupService) { - this.signupService = signupService; - } - - @PostMapping(value = "/signup") - public void signup(@RequestBody User user) { - signupService.signup(user); - } -} diff --git a/server/src/main/java/io/flexwork/statemachine/support/ErrorHandlingAction.java b/server/src/main/java/io/flexwork/statemachine/support/ErrorHandlingAction.java deleted file mode 100644 index 78505256..00000000 --- a/server/src/main/java/io/flexwork/statemachine/support/ErrorHandlingAction.java +++ /dev/null @@ -1,20 +0,0 @@ -package io.flexwork.statemachine.support; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.statemachine.StateContext; -import org.springframework.statemachine.action.Action; - -public class ErrorHandlingAction implements Action { - - private static final Logger LOG = LoggerFactory.getLogger(ErrorHandlingAction.class); - - @Override - public void execute(StateContext context) { - LOG.error( - "State machine {} has a error while executing at state {}", - context.getStateMachine().getId(), - context.getStateMachine().getState().getId(), - context.getException()); - } -} 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/001_initial_schema.xml similarity index 100% rename from tools/liquibase/src/main/resources/config/liquibase/master/changelog/00000000000000_initial_schema.xml rename to tools/liquibase/src/main/resources/config/liquibase/master/changelog/001_initial_schema.xml diff --git a/tools/liquibase/src/main/resources/config/liquibase/tenant/changelog/00000000000001_spring_state_machine_tables_created.xml b/tools/liquibase/src/main/resources/config/liquibase/tenant/changelog/00000000000001_spring_state_machine_tables_created.xml deleted file mode 100644 index b7b56821..00000000 --- a/tools/liquibase/src/main/resources/config/liquibase/tenant/changelog/00000000000001_spring_state_machine_tables_created.xml +++ /dev/null @@ -1,186 +0,0 @@ - - - - Spring State Machine JPA changelog - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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/000_initial_schema.xml similarity index 96% rename from tools/liquibase/src/main/resources/config/liquibase/tenant/changelog/00000000000000_initial_schema.xml rename to tools/liquibase/src/main/resources/config/liquibase/tenant/changelog/000_initial_schema.xml index 75866340..4bf6d4f4 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/000_initial_schema.xml @@ -6,8 +6,7 @@ http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd"> - + @@ -288,8 +287,7 @@ referencedColumnNames="id" /> - + + id="000:03-insert-default-authority-data"> @@ -314,7 +312,7 @@ + id="000:04-insert-default-user-data"> @@ -344,7 +342,7 @@ + id="000:05-insert-default-user-authority-data"> + id="000:06-insert-default-fw-team-data"> @@ -376,7 +374,7 @@ + id="000:07-insert-default-fw-team-role"> @@ -385,7 +383,7 @@ + id="000:08-insert-default-fw-user-team-data"> @@ -395,7 +393,7 @@ + id="000:09-insert-default-fw-resource"> @@ -403,7 +401,7 @@ + id="000:10-insert-default-fw-authority-resource-permission"> - + @@ -123,7 +122,7 @@ - + @@ -254,7 +253,7 @@ - + @@ -320,8 +319,7 @@ constraintName="fw_escalation_tracking_user" referencedTableName="fw_user" referencedColumnNames="id" /> - + @@ -331,7 +329,7 @@ + id="001:02-insert-workflow-state-data"> + id="001:03-insert-workflow-transition-data"> + id="001:04-insert-team-workflow-usage-data"> - + File storage system table design with closure table model - + @@ -294,8 +294,7 @@ - - -