diff --git a/backend/pom.xml b/backend/pom.xml
index e8c50967..805d78c8 100644
--- a/backend/pom.xml
+++ b/backend/pom.xml
@@ -129,6 +129,11 @@
1.18.30
true
+
+ org.ocpsoft.prettytime
+ prettytime
+ 5.0.7.Final
+
diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/common/exception/NotFoundGenericException.java b/backend/src/main/java/ca/bc/gov/restapi/results/common/exception/NotFoundGenericException.java
new file mode 100644
index 00000000..417c8efd
--- /dev/null
+++ b/backend/src/main/java/ca/bc/gov/restapi/results/common/exception/NotFoundGenericException.java
@@ -0,0 +1,14 @@
+package ca.bc.gov.restapi.results.common.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.server.ResponseStatusException;
+
+/** This class represents a generic not found request. */
+@ResponseStatus(value = HttpStatus.NOT_FOUND)
+public class NotFoundGenericException extends ResponseStatusException {
+
+ public NotFoundGenericException(String entityName) {
+ super(HttpStatus.NOT_FOUND, String.format("%s record(s) not found!", entityName));
+ }
+}
diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/common/exception/OpeningNotFoundException.java b/backend/src/main/java/ca/bc/gov/restapi/results/common/exception/OpeningNotFoundException.java
new file mode 100644
index 00000000..75bee872
--- /dev/null
+++ b/backend/src/main/java/ca/bc/gov/restapi/results/common/exception/OpeningNotFoundException.java
@@ -0,0 +1,9 @@
+package ca.bc.gov.restapi.results.common.exception;
+
+/** This class represents an error, when a Opening was not found. */
+public class OpeningNotFoundException extends NotFoundGenericException {
+
+ public OpeningNotFoundException() {
+ super("UserOpening");
+ }
+}
diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/common/exception/UserOpeningNotFoundException.java b/backend/src/main/java/ca/bc/gov/restapi/results/common/exception/UserOpeningNotFoundException.java
new file mode 100644
index 00000000..a6d69255
--- /dev/null
+++ b/backend/src/main/java/ca/bc/gov/restapi/results/common/exception/UserOpeningNotFoundException.java
@@ -0,0 +1,9 @@
+package ca.bc.gov.restapi.results.common.exception;
+
+/** This class represents a UserOpeningEntity not found in the database. */
+public class UserOpeningNotFoundException extends NotFoundGenericException {
+
+ public UserOpeningNotFoundException() {
+ super("UserOpeningEntity");
+ }
+}
diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/common/util/TimestampUtil.java b/backend/src/main/java/ca/bc/gov/restapi/results/common/util/TimestampUtil.java
new file mode 100644
index 00000000..a79a0528
--- /dev/null
+++ b/backend/src/main/java/ca/bc/gov/restapi/results/common/util/TimestampUtil.java
@@ -0,0 +1,56 @@
+package ca.bc.gov.restapi.results.common.util;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.Period;
+import java.time.format.DateTimeFormatter;
+import java.util.Objects;
+
+/** This class contains useful methods for parsing and handling timestamps. */
+public class TimestampUtil {
+
+ private TimestampUtil() {}
+
+ /**
+ * Parses a date string to a {@link LocalDateTime} instance. Format: yyyy-MM-dd.
+ *
+ * @param dateStr The date to be parsed
+ * @return LocalDateTime parsed or null if a null value is found.
+ */
+ public static LocalDateTime parseDateString(String dateStr) {
+ if (Objects.isNull(dateStr)) {
+ return null;
+ }
+
+ LocalDate entryDateStartLd =
+ LocalDate.parse(dateStr, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
+ return entryDateStartLd.atStartOfDay();
+ }
+
+ /**
+ * Extract the number based on the difference between today and the day the Opening got created.
+ *
+ * @param entryLocalDateTime The LocalDateTime representing the opening creation timestamp.
+ * @return An integer representing the index
+ */
+ public static int getLocalDateTimeIndex(LocalDateTime entryLocalDateTime) {
+ // index 0 -> 0 to 5 months
+ // index 1 -> 6 to 11 months
+ // index 2 -> 12 to 17 months
+ // index 3 -> 18+ months
+ LocalDate entryLocalDate = entryLocalDateTime.toLocalDate();
+ LocalDate now = LocalDate.now();
+
+ Period diff = Period.between(entryLocalDate, now);
+ int totalMonths = diff.getMonths() + (diff.getYears() * 12);
+ if (totalMonths <= 5) {
+ return 0;
+ } else if (totalMonths <= 11) {
+ return 1;
+ } else if (totalMonths <= 17) {
+ return 2;
+ } else {
+ return 3;
+ }
+ }
+}
diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/entity/OpeningEntity.java b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/entity/OpeningEntity.java
index eac1ea3a..784078a7 100644
--- a/backend/src/main/java/ca/bc/gov/restapi/results/oracle/entity/OpeningEntity.java
+++ b/backend/src/main/java/ca/bc/gov/restapi/results/oracle/entity/OpeningEntity.java
@@ -20,11 +20,9 @@ public class OpeningEntity {
private Long id;
@Column(name = "OPENING_STATUS_CODE", length = 3)
- // private OpeningStatusEnum status;
private String status;
@Column(name = "OPEN_CATEGORY_CODE", length = 7)
- // private OpeningCategoryEnum category;
private String category;
@Column(name = "ENTRY_USERID", length = 30)
diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/config/PostgresJpaConfig.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/config/PostgresJpaConfig.java
index 1065f5fd..2ece5ac0 100644
--- a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/config/PostgresJpaConfig.java
+++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/config/PostgresJpaConfig.java
@@ -48,7 +48,7 @@ public LocalContainerEntityManagerFactoryBean postgresEntityManagerFactory(
Properties jpaProps = new Properties();
jpaProps.setProperty("hibernate.default_schema", "silva");
jpaProps.setProperty("hibernate.ddl-auto", "update");
- jpaProps.setProperty("defer-datasource-initialization", "true");
+ jpaProps.setProperty("jpa.defer-datasource-initialization", "true");
jpaProps.setProperty("sql.init.mode", "always");
LocalContainerEntityManagerFactoryBean build =
diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/dto/OpeningsPerYearFiltersDto.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/dto/DashboardFiltesDto.java
similarity index 50%
rename from backend/src/main/java/ca/bc/gov/restapi/results/postgres/dto/OpeningsPerYearFiltersDto.java
rename to backend/src/main/java/ca/bc/gov/restapi/results/postgres/dto/DashboardFiltesDto.java
index 5d2a5dec..d6da148e 100644
--- a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/dto/OpeningsPerYearFiltersDto.java
+++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/dto/DashboardFiltesDto.java
@@ -3,5 +3,9 @@
import java.time.LocalDateTime;
/** This record contains all possible filters for the dashboard openings per years api. */
-public record OpeningsPerYearFiltersDto(
- String orgUnit, String status, LocalDateTime entryDateStart, LocalDateTime entryDateEnd) {}
+public record DashboardFiltesDto(
+ String orgUnit,
+ String status,
+ LocalDateTime entryDateStart,
+ LocalDateTime entryDateEnd,
+ String clientNumber) {}
diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/dto/FreeGrowingMilestonesDto.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/dto/FreeGrowingMilestonesDto.java
new file mode 100644
index 00000000..bb823946
--- /dev/null
+++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/dto/FreeGrowingMilestonesDto.java
@@ -0,0 +1,19 @@
+package ca.bc.gov.restapi.results.postgres.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.math.BigDecimal;
+
+/** This record represent a slice of the free growing milestone chart. */
+public record FreeGrowingMilestonesDto(
+ @Schema(description = "Number representing the index, between 0 and 3.", example = "1")
+ Integer index,
+ @Schema(
+ description = "Label of the current slice. E.g.: 0 - 5 month, 6 - 11 months",
+ example = "12 - 17 months")
+ String label,
+ @Schema(description = "Amount of openings in the slice", example = "33") Integer amount,
+ @Schema(
+ description =
+ "Percentage of this slice considering the total of openings on the period.",
+ example = "28")
+ BigDecimal percentage) {}
diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/dto/MyRecentActionsRequestsDto.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/dto/MyRecentActionsRequestsDto.java
new file mode 100644
index 00000000..fa317ae8
--- /dev/null
+++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/dto/MyRecentActionsRequestsDto.java
@@ -0,0 +1,34 @@
+package ca.bc.gov.restapi.results.postgres.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.time.LocalDateTime;
+
+/** This record represents an opening activity in the requests tab. */
+public record MyRecentActionsRequestsDto(
+ @Schema(description = "Full description of the action made by the user.", example = "Update")
+ String activityType,
+ @Schema(
+ description = "System generated value uniquely identifying the opening.",
+ example = "1541297")
+ Long openingId,
+ @Schema(
+ description =
+ """
+ A code indicating the status of the prescription. Examples include but are not
+ limited to DFT (draft) and APP (approved). A subset of the STATUS_CODE table.
+ """,
+ example = "APP")
+ String statusCode,
+ @Schema(
+ description =
+ """
+ The code description indicating the status of the prescription. Examples include but
+ are not limited to Draft (DFT) and Approved (APP). A subset of the STATUS_CODE table.
+ """,
+ example = "Approved")
+ String statusDescription,
+ @Schema(
+ description = "The date and time of the activity action in for 'time ago' format",
+ example = "1 minute ago")
+ String lastUpdatedLabel,
+ @Schema(description = "The date and time of the activity action") LocalDateTime lastUpdated) {}
diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/endpoint/DashboardMetricsEndpoint.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/endpoint/DashboardMetricsEndpoint.java
index 0c48ad1f..d33e5895 100644
--- a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/endpoint/DashboardMetricsEndpoint.java
+++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/endpoint/DashboardMetricsEndpoint.java
@@ -1,7 +1,10 @@
package ca.bc.gov.restapi.results.postgres.endpoint;
+import ca.bc.gov.restapi.results.common.util.TimestampUtil;
+import ca.bc.gov.restapi.results.postgres.dto.DashboardFiltesDto;
+import ca.bc.gov.restapi.results.postgres.dto.FreeGrowingMilestonesDto;
+import ca.bc.gov.restapi.results.postgres.dto.MyRecentActionsRequestsDto;
import ca.bc.gov.restapi.results.postgres.dto.OpeningsPerYearDto;
-import ca.bc.gov.restapi.results.postgres.dto.OpeningsPerYearFiltersDto;
import ca.bc.gov.restapi.results.postgres.service.DashboardMetricsService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@@ -10,11 +13,7 @@
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
-import java.time.LocalDate;
-import java.time.LocalDateTime;
-import java.time.format.DateTimeFormatter;
import java.util.List;
-import java.util.Objects;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
@@ -25,32 +24,36 @@
/** This class holds resources for the dashboard metrics page. */
@RestController
@RequestMapping("/api/dashboard-metrics")
-@Tag(name = "Dashboard Metrics", description = "Resources fot the Dashboard metrics charts")
+@Tag(
+ name = "Dashboard Metrics (SILVA)",
+ description = "Endpoints fot the Dashboard metrics charts in the `SILVA` schema")
@RequiredArgsConstructor
public class DashboardMetricsEndpoint {
private final DashboardMetricsService dashboardMetricsService;
/**
- * Get data for the Submission Trends Chart, Openings per Year.
+ * Gets data for the Opening submission trends Chart (Openings per year) on the Dashboard SILVA
+ * page.
*
- * @param orgUnitCode The district code to filter.
- * @param statusCode The opening status code to filter.
- * @param entryDateStart The opening entry timestamp start date filter.
- * @param entryDateEnd The opening entry timestamp end date filter.
+ * @param orgUnitCode Optional district code filter.
+ * @param statusCode Optional opening status code filter.
+ * @param entryDateStart Optional opening entry timestamp start date filter.
+ * @param entryDateEnd Optional opening entry timestamp end date filter.
* @return A list of values to populate the chart or 204 no content if no data.
*/
@GetMapping("/submission-trends")
@Operation(
- summary = "Get data for the Submission Trends Chart, Openings per Year",
- description = "Fetches data from the last years for the openings per year chart.",
+ summary = "Gets data for the Opening submission trends Chart (Openings per year).",
+ description = "Fetches data from the last twelve months for the openings per year chart.",
responses = {
@ApiResponse(
responseCode = "200",
- description = "An array with twelve objects for the last 12 months."),
+ description = "An array with twelve objects for the last 12 months.",
+ content = @Content(mediaType = "application/json")),
@ApiResponse(
responseCode = "204",
- description = "No data found in the dable. No response body."),
+ description = "No data found on the table. No response body."),
@ApiResponse(
responseCode = "401",
description = "Access token is missing or invalid",
@@ -89,31 +92,132 @@ public ResponseEntity> getOpeningsSubmissionTrends(
required = false,
example = "2024-03-11")
String entryDateEnd) {
- LocalDateTime entryDateStartDate = null;
- LocalDateTime entryDateEndDate = null;
- DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+ DashboardFiltesDto filtersDto =
+ new DashboardFiltesDto(
+ orgUnitCode,
+ statusCode,
+ TimestampUtil.parseDateString(entryDateStart),
+ TimestampUtil.parseDateString(entryDateEnd),
+ null);
- if (!Objects.isNull(entryDateStart)) {
- LocalDate entryDateStartLd = LocalDate.parse(entryDateStart, fmt);
- entryDateStartDate = entryDateStartLd.atStartOfDay();
+ List resultList =
+ dashboardMetricsService.getOpeningsSubmissionTrends(filtersDto);
+
+ if (resultList.isEmpty()) {
+ return ResponseEntity.noContent().build();
}
- if (!Objects.isNull(entryDateEnd)) {
- LocalDate entryDateEndLd = LocalDate.parse(entryDateEnd, fmt);
- entryDateEndDate = entryDateEndLd.atStartOfDay();
+ return ResponseEntity.ok(resultList);
+ }
+
+ /**
+ * Gets data for the Free growing Chart on the Dashboard SILVA page.
+ *
+ * @param orgUnitCode Optional district code filter.
+ * @param clientNumber Optional client number filter.
+ * @param entryDateStart Optional opening entry timestamp start date filter.
+ * @param entryDateEnd Optional opening entry timestamp end date filter.
+ * @return A list of values to populate the chart or 204 no content if no data.
+ */
+ @GetMapping("/free-growing-milestones")
+ @Operation(
+ summary = "Gets data for the Free growing Chart on the Dashboard SILVA page.",
+ description =
+ "Fetches data from the last 24 months and group them into periods for the Free growing"
+ + " chart.",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "An array with four objects, one for each piece of the chart.",
+ content = @Content(mediaType = "application/json")),
+ @ApiResponse(
+ responseCode = "204",
+ description = "No data found on the table. No response body."),
+ @ApiResponse(
+ responseCode = "401",
+ description = "Access token is missing or invalid",
+ content = @Content(schema = @Schema(implementation = Void.class)))
+ })
+ public ResponseEntity> getFreeGrowingMilestonesData(
+ @RequestParam(value = "orgUnitCode", required = false)
+ @Parameter(
+ name = "orgUnitCode",
+ in = ParameterIn.QUERY,
+ description = "The Org Unit code to filter, same as District",
+ required = false,
+ example = "DCR")
+ String orgUnitCode,
+ @RequestParam(value = "clientNumber", required = false)
+ @Parameter(
+ name = "clientNumber",
+ in = ParameterIn.QUERY,
+ description = "The Client Number to filter",
+ required = false,
+ example = "00012797")
+ String clientNumber,
+ @RequestParam(value = "entryDateStart", required = false)
+ @Parameter(
+ name = "entryDateStart",
+ in = ParameterIn.QUERY,
+ description = "The Openins entry timestamp start date to filter, format yyyy-MM-dd",
+ required = false,
+ example = "2024-03-11")
+ String entryDateStart,
+ @RequestParam(value = "entryDateEnd", required = false)
+ @Parameter(
+ name = "entryDateEnd",
+ in = ParameterIn.QUERY,
+ description = "The Openins entry timestamp end date to filter, format yyyy-MM-dd",
+ required = false,
+ example = "2024-03-11")
+ String entryDateEnd) {
+ DashboardFiltesDto filtersDto =
+ new DashboardFiltesDto(
+ orgUnitCode,
+ null,
+ TimestampUtil.parseDateString(entryDateStart),
+ TimestampUtil.parseDateString(entryDateEnd),
+ clientNumber);
+ List milestonesDto =
+ dashboardMetricsService.getFreeGrowingMilestoneChartData(filtersDto);
+
+ if (milestonesDto.isEmpty()) {
+ return ResponseEntity.noContent().build();
}
- OpeningsPerYearFiltersDto filtersDto =
- new OpeningsPerYearFiltersDto(
- orgUnitCode, statusCode, entryDateStartDate, entryDateEndDate);
+ return ResponseEntity.ok(milestonesDto);
+ }
- List resultList =
- dashboardMetricsService.getOpeningsSubmissionTrends(filtersDto);
+ /**
+ * Gets the last 5 most recent updated openings for the request user.
+ *
+ * @return A list of values to populate the chart or 204 no content if no data.
+ */
+ @GetMapping("/my-recent-actions/requests")
+ @Operation(
+ summary = "Gets the last 5 most recent updated openings for the request user.",
+ description = "Fetches data for the My recent actions table, Requests tab",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "An array with five objects, one for opening row.",
+ content = @Content(mediaType = "application/json")),
+ @ApiResponse(
+ responseCode = "204",
+ description = "No data found for the user. No response body."),
+ @ApiResponse(
+ responseCode = "401",
+ description = "Access token is missing or invalid",
+ content = @Content(schema = @Schema(implementation = Void.class)))
+ })
+ public ResponseEntity> getUserRecentOpeningsActions() {
+ List actionsDto =
+ dashboardMetricsService.getUserRecentOpeningsActions();
- if (resultList.isEmpty()) {
+ if (actionsDto.isEmpty()) {
return ResponseEntity.noContent().build();
}
- return ResponseEntity.ok(resultList);
+ return ResponseEntity.ok(actionsDto);
}
}
diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/endpoint/UserOpeningEndpoint.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/endpoint/UserOpeningEndpoint.java
index 0b156b38..3f783bab 100644
--- a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/endpoint/UserOpeningEndpoint.java
+++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/endpoint/UserOpeningEndpoint.java
@@ -1,30 +1,129 @@
package ca.bc.gov.restapi.results.postgres.endpoint;
-import ca.bc.gov.restapi.results.postgres.entity.UserOpeningEntity;
-import ca.bc.gov.restapi.results.postgres.repository.UserOpeningRepository;
+import ca.bc.gov.restapi.results.postgres.dto.MyRecentActionsRequestsDto;
+import ca.bc.gov.restapi.results.postgres.service.UserOpeningService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.enums.ParameterIn;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
-import org.springframework.http.MediaType;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/** This class holds resources for exposing user openings saved as favourites. */
@RestController
-@RequestMapping(path = "/api/user-openings", produces = MediaType.APPLICATION_JSON_VALUE)
+@RequestMapping("/api/user-openings")
@Tag(
- name = "User Opennings (SILVA)",
+ name = "User Openings (SILVA)",
description = "Endpoints to handle user favourite Openings in the `SILVA` schema.")
+@RequiredArgsConstructor
public class UserOpeningEndpoint {
- private final UserOpeningRepository userOpeningRepository;
+ private final UserOpeningService userOpeningService;
- UserOpeningEndpoint(UserOpeningRepository userOpeningRepository) {
- this.userOpeningRepository = userOpeningRepository;
+ /**
+ * Gets up to three tracked Openings to a user.
+ *
+ * @return A list of openings or the http code 204-No Content.
+ */
+ @GetMapping("/dashboard-track-openings")
+ @Operation(
+ summary = "Gets up to three updated Openings to a user",
+ description = "Gets up to three updated openings that got saved by the user.",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "An array containing up to three Openings for the user.",
+ content = @Content(mediaType = "application/json")),
+ @ApiResponse(
+ responseCode = "204",
+ description = "No data found for this user. No response body."),
+ @ApiResponse(
+ responseCode = "401",
+ description = "Access token is missing or invalid",
+ content = @Content(schema = @Schema(implementation = Void.class)))
+ })
+ public ResponseEntity> getUserTrackedOpenings() {
+ List userOpenings = userOpeningService.getUserTrackedOpenings();
+ if (userOpenings.isEmpty()) {
+ return ResponseEntity.noContent().build();
+ }
+
+ return ResponseEntity.ok(userOpenings);
+ }
+
+ /**
+ * Saves one Opening ID as favourite to an user.
+ *
+ * @param id The opening ID.
+ * @return HTTP status code 201 if success, no response body.
+ */
+ @PostMapping("/{id}")
+ @Operation(
+ summary = "Saves one Opening ID as favourite to an user",
+ description = "Allow users to save Opening IDs to track them easily in the dashboard.",
+ responses = {
+ @ApiResponse(responseCode = "201", description = "Opening successfully saved to the user."),
+ @ApiResponse(
+ responseCode = "401",
+ description = "Access token is missing or invalid",
+ content = @Content(schema = @Schema(implementation = Void.class)))
+ })
+ public ResponseEntity saveUserOpening(
+ @Parameter(
+ name = "id",
+ in = ParameterIn.PATH,
+ description = "Opening ID",
+ required = true,
+ schema = @Schema(type = "integer", format = "int64"))
+ @PathVariable
+ Long id) {
+ userOpeningService.saveOpeningToUser(id);
+ return ResponseEntity.status(HttpStatus.CREATED).build();
}
- @GetMapping
- public List getAll() {
- return userOpeningRepository.findAll();
+ /**
+ * Deletes one or more user openings from their favourite list.
+ *
+ * @param id The opening ID.
+ * @return HTTP status code 204 if success, no response body.
+ */
+ @DeleteMapping("/{id}")
+ @Operation(
+ summary = "Deleted an Opening ID from the user's favourite",
+ description = "Allow users to remove saved Opening IDs from their favourite list.",
+ responses = {
+ @ApiResponse(
+ responseCode = "204",
+ description = "Opening successfully removed. No content body."),
+ @ApiResponse(
+ responseCode = "401",
+ description = "Access token is missing or invalid",
+ content = @Content(schema = @Schema(implementation = Void.class))),
+ @ApiResponse(
+ responseCode = "404",
+ description = "Opening not found in the user's favourite.")
+ })
+ public ResponseEntity deleteUserOpening(
+ @Parameter(
+ name = "id",
+ in = ParameterIn.PATH,
+ description = "Opening ID",
+ required = true,
+ schema = @Schema(type = "integer", format = "int64"))
+ @PathVariable
+ Long id) {
+ userOpeningService.deleteOpeningFromUserFavourite(id);
+ return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}
}
diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/OpeningsActivityEntity.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/OpeningsActivityEntity.java
new file mode 100644
index 00000000..d33a8878
--- /dev/null
+++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/OpeningsActivityEntity.java
@@ -0,0 +1,39 @@
+package ca.bc.gov.restapi.results.postgres.entity;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import java.time.LocalDateTime;
+import lombok.Getter;
+import lombok.Setter;
+
+/** This class represents a record in the database for the openings_activity table. */
+@Getter
+@Setter
+@Entity
+@Table(name = "openings_activity")
+public class OpeningsActivityEntity {
+
+ @Id
+ @Column(name = "opening_id")
+ private Long openingId;
+
+ @Column(name = "activity_type_code", length = 3)
+ private String activityTypeCode;
+
+ @Column(name = "activity_type_desc", length = 120)
+ private String activityTypeDesc;
+
+ @Column(name = "status_code", length = 3, nullable = false)
+ private String statusCode;
+
+ @Column(name = "status_desc", length = 120, nullable = false)
+ private String statusDesc;
+
+ @Column(name = "last_updated", nullable = false)
+ private LocalDateTime lastUpdated;
+
+ @Column(name = "entry_userid", length = 30, nullable = false)
+ private String entryUserid;
+}
diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/OpeningsLastYearEntity.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/OpeningsLastYearEntity.java
index 5950a4e5..60360441 100644
--- a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/OpeningsLastYearEntity.java
+++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/OpeningsLastYearEntity.java
@@ -17,9 +17,9 @@ public class OpeningsLastYearEntity {
@Id
@Column(name = "opening_id")
- private String openingId;
+ private Long openingId;
- @Column(name = "opening_entry_userid", nullable = false)
+ @Column(name = "opening_entry_userid", nullable = false, length = 30)
private String userId;
@Column(name = "entry_timestamp", nullable = false)
@@ -28,9 +28,12 @@ public class OpeningsLastYearEntity {
@Column(name = "update_timestamp", nullable = false)
private LocalDateTime updateTimestamp;
- @Column(name = "status_code", nullable = false)
+ @Column(name = "status_code", nullable = false, length = 3)
private String status;
- @Column(name = "org_unit_code", nullable = false)
+ @Column(name = "org_unit_code", nullable = false, length = 6)
private String orgUnitCode;
+
+ @Column(name = "client_number", nullable = false, length = 8)
+ private String clientNumber;
}
diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/UserOpeningEntity.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/UserOpeningEntity.java
index ad10a717..2ab815fa 100644
--- a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/UserOpeningEntity.java
+++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/UserOpeningEntity.java
@@ -26,5 +26,5 @@ public class UserOpeningEntity {
@Id
@Column(name = "opening_id")
- private String openingId;
+ private Long openingId;
}
diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/UserOpeningEntityId.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/UserOpeningEntityId.java
index 343493c7..e7af80be 100644
--- a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/UserOpeningEntityId.java
+++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/entity/UserOpeningEntityId.java
@@ -19,5 +19,5 @@
public class UserOpeningEntityId {
@NonNull private String userId;
- @NonNull private String openingId;
+ @NonNull private Long openingId;
}
diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/repository/OpeningsActivityRepository.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/repository/OpeningsActivityRepository.java
new file mode 100644
index 00000000..c173c972
--- /dev/null
+++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/repository/OpeningsActivityRepository.java
@@ -0,0 +1,12 @@
+package ca.bc.gov.restapi.results.postgres.repository;
+
+import ca.bc.gov.restapi.results.postgres.entity.OpeningsActivityEntity;
+import java.util.List;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+/** This interface provides access to the database for the OpeningsActivityEntity entity. */
+public interface OpeningsActivityRepository extends JpaRepository {
+
+ List findAllByEntryUserid(String userId, Sort sort);
+}
diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/repository/OpeningsLastYearRepository.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/repository/OpeningsLastYearRepository.java
index 418cd656..ce7d34cd 100644
--- a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/repository/OpeningsLastYearRepository.java
+++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/repository/OpeningsLastYearRepository.java
@@ -1,7 +1,13 @@
package ca.bc.gov.restapi.results.postgres.repository;
import ca.bc.gov.restapi.results.postgres.entity.OpeningsLastYearEntity;
+import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
/** This interface provides access to the database for the OpeningsLastYearEntity entity. */
-public interface OpeningsLastYearRepository extends JpaRepository {}
+public interface OpeningsLastYearRepository extends JpaRepository {
+
+ @Query("from OpeningsLastYearEntity o where o.openingId in ?1")
+ List findAllByOpeningIdInList(List openingIdList);
+}
diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/repository/UserOpeningRepository.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/repository/UserOpeningRepository.java
index fb208a22..377be33c 100644
--- a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/repository/UserOpeningRepository.java
+++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/repository/UserOpeningRepository.java
@@ -2,8 +2,16 @@
import ca.bc.gov.restapi.results.postgres.entity.UserOpeningEntity;
import ca.bc.gov.restapi.results.postgres.entity.UserOpeningEntityId;
+import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
/** This interface holds methods for handling {@link UserOpeningEntity} data in the database. */
public interface UserOpeningRepository
- extends JpaRepository {}
+ extends JpaRepository {
+
+ List findAllByUserId(String userId);
+
+ @Query("from UserOpeningEntity o where o.openingId in ?1 and o.userId = ?2")
+ List findAllByOpeningIdInAndUserId(List openingIdList, String userId);
+}
diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/service/DashboardMetricsService.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/service/DashboardMetricsService.java
index 09eee9dc..f71a7e75 100644
--- a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/service/DashboardMetricsService.java
+++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/service/DashboardMetricsService.java
@@ -1,9 +1,17 @@
package ca.bc.gov.restapi.results.postgres.service;
+import ca.bc.gov.restapi.results.common.security.LoggedUserService;
+import ca.bc.gov.restapi.results.common.util.TimestampUtil;
+import ca.bc.gov.restapi.results.postgres.dto.DashboardFiltesDto;
+import ca.bc.gov.restapi.results.postgres.dto.FreeGrowingMilestonesDto;
+import ca.bc.gov.restapi.results.postgres.dto.MyRecentActionsRequestsDto;
import ca.bc.gov.restapi.results.postgres.dto.OpeningsPerYearDto;
-import ca.bc.gov.restapi.results.postgres.dto.OpeningsPerYearFiltersDto;
+import ca.bc.gov.restapi.results.postgres.entity.OpeningsActivityEntity;
import ca.bc.gov.restapi.results.postgres.entity.OpeningsLastYearEntity;
+import ca.bc.gov.restapi.results.postgres.repository.OpeningsActivityRepository;
import ca.bc.gov.restapi.results.postgres.repository.OpeningsLastYearRepository;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
import java.time.Month;
import java.util.ArrayList;
import java.util.HashMap;
@@ -13,6 +21,7 @@
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.ocpsoft.prettytime.PrettyTime;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
@@ -24,13 +33,17 @@ public class DashboardMetricsService {
private final OpeningsLastYearRepository openingsLastYearRepository;
+ private final OpeningsActivityRepository openingsActivityRepository;
+
+ private final LoggedUserService loggedUserService;
+
/**
* Get openings submission trends data for the opening per year chart.
*
- * @param filters Possible filter, see {@link OpeningsPerYearFiltersDto} for more.
+ * @param filters Possible filter, see {@link DashboardFiltesDto} for more.
* @return A list of {@link OpeningsPerYearDto} for the opening chart.
*/
- public List getOpeningsSubmissionTrends(OpeningsPerYearFiltersDto filters) {
+ public List getOpeningsSubmissionTrends(DashboardFiltesDto filters) {
log.info("Getting Opening Submission Trends with filters {}", filters.toString());
List entities =
@@ -60,6 +73,7 @@ public List getOpeningsSubmissionTrends(OpeningsPerYearFilte
}
}
+ // Iterate over the found records filtering and putting them into the right month
for (OpeningsLastYearEntity entity : entities) {
// Org Unit filter - District
if (!Objects.isNull(filters.orgUnit())
@@ -91,10 +105,153 @@ public List getOpeningsSubmissionTrends(OpeningsPerYearFilte
for (Integer monthKey : resultMap.keySet()) {
List monthDataList = resultMap.get(monthKey);
String monthName = monthNamesMap.get(monthKey);
- log.info("Month: {}", monthName);
+ log.info("Value {} for the month: {}", monthDataList.size(), monthName);
chartData.add(new OpeningsPerYearDto(monthKey, monthName, monthDataList.size()));
}
return chartData;
}
+
+ /**
+ * Get free growing milestone declarations data for the chart.
+ *
+ * @param filters Possible filter, see {@link DashboardFiltesDto} for more.
+ * @return A list of {@link FreeGrowingMilestonesDto} for the chart.
+ */
+ public List getFreeGrowingMilestoneChartData(
+ DashboardFiltesDto filters) {
+ log.info("Getting Free growing milestones with filters {}", filters.toString());
+
+ List entities =
+ openingsLastYearRepository.findAll(Sort.by("openingId").ascending());
+
+ if (entities.isEmpty()) {
+ log.info("No Free growing milestones data found!");
+ return List.of();
+ }
+
+ Map> resultMap = new LinkedHashMap<>();
+
+ // Fill with all four pieces
+ for (int i = 0; i < 4; i++) {
+ resultMap.put(i, new ArrayList<>());
+ }
+
+ Map labelsMap = new HashMap<>();
+ labelsMap.put(0, "0 - 5 months");
+ labelsMap.put(1, "6 - 11 months");
+ labelsMap.put(2, "12 - 17 months");
+ labelsMap.put(3, "18 months");
+
+ int totalRecordsFiltered = 0;
+
+ // Iterate over the found records filtering and putting them into the right piece
+ for (OpeningsLastYearEntity entity : entities) {
+ // Org Unit filter - District
+ if (!Objects.isNull(filters.orgUnit())
+ && !entity.getOrgUnitCode().equals(filters.orgUnit())) {
+ continue;
+ }
+
+ // Client number
+ if (!Objects.isNull(filters.clientNumber())
+ && !entity.getClientNumber().equals(filters.clientNumber())) {
+ continue;
+ }
+
+ // Entry start date filter
+ if (!Objects.isNull(filters.entryDateStart())
+ && entity.getEntryTimestamp().isBefore(filters.entryDateStart())) {
+ continue;
+ }
+
+ // Entry end date filter
+ if (!Objects.isNull(filters.entryDateEnd())
+ && entity.getEntryTimestamp().isAfter(filters.entryDateEnd())) {
+ continue;
+ }
+
+ int index = TimestampUtil.getLocalDateTimeIndex(entity.getEntryTimestamp());
+ resultMap.get(index).add(entity);
+ totalRecordsFiltered++;
+ }
+
+ List chartData = new ArrayList<>();
+ BigDecimal hundred = new BigDecimal("100");
+ BigDecimal hundredSum = new BigDecimal("100");
+ for (Integer index : resultMap.keySet()) {
+ List groupList = resultMap.get(index);
+ String label = labelsMap.get(index);
+ int value = groupList.size();
+ log.info("{} openings of {} for label '{}'", value, totalRecordsFiltered, label);
+
+ BigDecimal percentage = BigDecimal.ZERO;
+ if (totalRecordsFiltered > 0) {
+ percentage =
+ new BigDecimal(String.valueOf(value))
+ .divide(
+ new BigDecimal(String.valueOf(totalRecordsFiltered)), 10, RoundingMode.HALF_UP)
+ .setScale(2, RoundingMode.HALF_UP)
+ .multiply(hundred);
+ }
+
+ if (index < 3) {
+ hundredSum = hundredSum.subtract(percentage);
+ } else if (index == 3 && totalRecordsFiltered > 0) {
+ percentage = hundredSum;
+ }
+
+ log.info("Percentage {}% for the label: {}", percentage, label);
+ chartData.add(new FreeGrowingMilestonesDto(index, label, value, percentage));
+ }
+
+ return chartData;
+ }
+
+ /**
+ * Get My recent actions table data for the Request tabs.
+ *
+ * @return A list of {@link MyRecentActionsRequestsDto} with 5 rows.
+ */
+ public List getUserRecentOpeningsActions() {
+ log.info("Getting up to the last 5 openings activities for the requests tab");
+
+ String userId = loggedUserService.getLoggedUserId();
+
+ Sort sort = Sort.by("lastUpdated").descending();
+ List openingList =
+ openingsActivityRepository.findAllByEntryUserid(userId, sort);
+
+ if (openingList.isEmpty()) {
+ log.info("No recent activities data found for the user!");
+ return List.of();
+ }
+
+ PrettyTime prettyTime = new PrettyTime();
+
+ List chartData = new ArrayList<>();
+ for (OpeningsActivityEntity entity : openingList) {
+ String statusDesc = entity.getActivityTypeDesc();
+ if (Objects.isNull(statusDesc)) {
+ statusDesc = "Created";
+ }
+
+ MyRecentActionsRequestsDto dto =
+ new MyRecentActionsRequestsDto(
+ statusDesc,
+ entity.getOpeningId(),
+ entity.getStatusCode(),
+ entity.getStatusDesc(),
+ prettyTime.format(entity.getLastUpdated()),
+ entity.getLastUpdated());
+
+ chartData.add(dto);
+
+ if (chartData.size() == 5) {
+ break;
+ }
+ }
+
+ return chartData;
+ }
}
diff --git a/backend/src/main/java/ca/bc/gov/restapi/results/postgres/service/UserOpeningService.java b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/service/UserOpeningService.java
new file mode 100644
index 00000000..5175b3e2
--- /dev/null
+++ b/backend/src/main/java/ca/bc/gov/restapi/results/postgres/service/UserOpeningService.java
@@ -0,0 +1,123 @@
+package ca.bc.gov.restapi.results.postgres.service;
+
+import ca.bc.gov.restapi.results.common.exception.UserOpeningNotFoundException;
+import ca.bc.gov.restapi.results.common.security.LoggedUserService;
+import ca.bc.gov.restapi.results.postgres.dto.MyRecentActionsRequestsDto;
+import ca.bc.gov.restapi.results.postgres.entity.OpeningsActivityEntity;
+import ca.bc.gov.restapi.results.postgres.entity.UserOpeningEntity;
+import ca.bc.gov.restapi.results.postgres.entity.UserOpeningEntityId;
+import ca.bc.gov.restapi.results.postgres.repository.OpeningsActivityRepository;
+import ca.bc.gov.restapi.results.postgres.repository.UserOpeningRepository;
+import jakarta.transaction.Transactional;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.ocpsoft.prettytime.PrettyTime;
+import org.springframework.stereotype.Service;
+
+/** This class contains methods for handling User favourite Openings. */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class UserOpeningService {
+
+ private final LoggedUserService loggedUserService;
+
+ private final UserOpeningRepository userOpeningRepository;
+
+ private final OpeningsActivityRepository openingsActivityRepository;
+
+ /**
+ * Gets user's tracked Openings.
+ *
+ * @return A list of {@link MyRecentActionsRequestsDto} containing the found records.
+ */
+ public List getUserTrackedOpenings() {
+ log.info("Getting all user openings for the Track openings table");
+
+ String userId = loggedUserService.getLoggedUserId();
+ List userList = userOpeningRepository.findAllByUserId(userId);
+
+ if (userList.isEmpty()) {
+ log.info("No saved openings for the current user!");
+ return List.of();
+ }
+
+ List openingIds = userList.stream().map(UserOpeningEntity::getOpeningId).toList();
+ List openingActivities =
+ openingsActivityRepository.findAllById(openingIds);
+
+ if (openingActivities.isEmpty()) {
+ log.info("No records found on the opening activity table for the opening ID list!");
+ return List.of();
+ }
+
+ List resultList = new ArrayList<>();
+
+ PrettyTime prettyTime = new PrettyTime();
+
+ for (OpeningsActivityEntity activityEntity : openingActivities) {
+ MyRecentActionsRequestsDto requestsDto =
+ new MyRecentActionsRequestsDto(
+ activityEntity.getActivityTypeDesc(),
+ activityEntity.getOpeningId(),
+ activityEntity.getStatusCode(),
+ activityEntity.getStatusDesc(),
+ prettyTime.format(activityEntity.getLastUpdated()),
+ activityEntity.getLastUpdated());
+
+ resultList.add(requestsDto);
+
+ if (resultList.size() == 3) {
+ break;
+ }
+ }
+
+ return resultList;
+ }
+
+ /**
+ * Saves one or more Openings IDs to an user.
+ *
+ * @param openingId The opening ID.
+ */
+ @Transactional
+ public void saveOpeningToUser(Long openingId) {
+ log.info("Opening ID to save in the user favourites: {}", openingId);
+
+ final String userId = loggedUserService.getLoggedUserId();
+
+ UserOpeningEntity entity = new UserOpeningEntity();
+ entity.setUserId(userId);
+ entity.setOpeningId(openingId);
+
+ userOpeningRepository.saveAndFlush(entity);
+ log.info("Opening ID saved in the user's favourites!");
+ }
+
+ /**
+ * Deletes one or more user opening from favourite.
+ *
+ * @param openingId The opening ID.
+ */
+ @Transactional
+ public void deleteOpeningFromUserFavourite(Long openingId) {
+ log.info("Opening ID to delete from the user's favourites: {}", openingId);
+ String userId = loggedUserService.getLoggedUserId();
+
+ UserOpeningEntityId openingPk = new UserOpeningEntityId(userId, openingId);
+
+ Optional userOpeningsOp = userOpeningRepository.findById(openingPk);
+
+ if (userOpeningsOp.isEmpty()) {
+ log.info("Opening id {} not found in the user's favourite list!", openingId);
+ throw new UserOpeningNotFoundException();
+ }
+
+ userOpeningRepository.delete(userOpeningsOp.get());
+ userOpeningRepository.flush();
+ log.info("Opening ID deleted from the favourites!");
+ }
+}
diff --git a/backend/src/main/resources/db/migration/V5__add_client_number_metrics_table.sql b/backend/src/main/resources/db/migration/V5__add_client_number_metrics_table.sql
new file mode 100644
index 00000000..0e87629a
--- /dev/null
+++ b/backend/src/main/resources/db/migration/V5__add_client_number_metrics_table.sql
@@ -0,0 +1,17 @@
+alter table silva.openings_last_year
+ add column client_number VARCHAR(8) NOT NULL;
+
+-- drop table and recreate fixing the table name and columns
+drop table silva.openings_activitiy;
+
+create table IF NOT EXISTS silva.openings_activity (
+ opening_id DECIMAL(10,0) NOT NULL,
+ activity_type_code VARCHAR(3),
+ activity_type_desc VARCHAR(120),
+ status_code VARCHAR(3) NOT NULL,
+ status_desc VARCHAR(120) NOT NULL,
+ last_updated TIMESTAMP NOT NULL,
+ entry_userid VARCHAR(30) NOT NULL,
+ constraint openings_activitiy_pk
+ primary key(opening_id)
+);
diff --git a/backend/src/test/java/ca/bc/gov/restapi/results/common/util/TimestampUtilTest.java b/backend/src/test/java/ca/bc/gov/restapi/results/common/util/TimestampUtilTest.java
new file mode 100644
index 00000000..c885e0b6
--- /dev/null
+++ b/backend/src/test/java/ca/bc/gov/restapi/results/common/util/TimestampUtilTest.java
@@ -0,0 +1,45 @@
+package ca.bc.gov.restapi.results.common.util;
+
+import java.time.LocalDateTime;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class TimestampUtilTest {
+
+ @Test
+ void parseDateStringTest() {
+ LocalDateTime dateTimeParsed = TimestampUtil.parseDateString("2024-04-09");
+ Assertions.assertNotNull(dateTimeParsed);
+ Assertions.assertEquals(2024, dateTimeParsed.getYear());
+ Assertions.assertEquals(4, dateTimeParsed.getMonthValue());
+ Assertions.assertEquals(9, dateTimeParsed.getDayOfMonth());
+
+ LocalDateTime dateTimeNull = TimestampUtil.parseDateString(null);
+ Assertions.assertNull(dateTimeNull);
+ }
+
+ @Test
+ void getLocalDateTimeIndexTest() {
+ LocalDateTime now = LocalDateTime.now();
+
+ Assertions.assertEquals(0, TimestampUtil.getLocalDateTimeIndex(now.minusMonths(1L)));
+ Assertions.assertEquals(0, TimestampUtil.getLocalDateTimeIndex(now.minusMonths(2L)));
+ Assertions.assertEquals(0, TimestampUtil.getLocalDateTimeIndex(now.minusMonths(3L)));
+ Assertions.assertEquals(0, TimestampUtil.getLocalDateTimeIndex(now.minusMonths(4L)));
+ Assertions.assertEquals(0, TimestampUtil.getLocalDateTimeIndex(now.minusMonths(5L)));
+ Assertions.assertEquals(1, TimestampUtil.getLocalDateTimeIndex(now.minusMonths(6L)));
+ Assertions.assertEquals(1, TimestampUtil.getLocalDateTimeIndex(now.minusMonths(7L)));
+ Assertions.assertEquals(1, TimestampUtil.getLocalDateTimeIndex(now.minusMonths(8L)));
+ Assertions.assertEquals(1, TimestampUtil.getLocalDateTimeIndex(now.minusMonths(9L)));
+ Assertions.assertEquals(1, TimestampUtil.getLocalDateTimeIndex(now.minusMonths(10L)));
+ Assertions.assertEquals(1, TimestampUtil.getLocalDateTimeIndex(now.minusMonths(11L)));
+ Assertions.assertEquals(2, TimestampUtil.getLocalDateTimeIndex(now.minusMonths(12L)));
+ Assertions.assertEquals(2, TimestampUtil.getLocalDateTimeIndex(now.minusMonths(13L)));
+ Assertions.assertEquals(2, TimestampUtil.getLocalDateTimeIndex(now.minusMonths(14L)));
+ Assertions.assertEquals(2, TimestampUtil.getLocalDateTimeIndex(now.minusMonths(15L)));
+ Assertions.assertEquals(2, TimestampUtil.getLocalDateTimeIndex(now.minusMonths(16L)));
+ Assertions.assertEquals(2, TimestampUtil.getLocalDateTimeIndex(now.minusMonths(17L)));
+ Assertions.assertEquals(3, TimestampUtil.getLocalDateTimeIndex(now.minusMonths(18L)));
+ Assertions.assertEquals(3, TimestampUtil.getLocalDateTimeIndex(now.minusMonths(36L)));
+ }
+}
diff --git a/backend/src/test/java/ca/bc/gov/restapi/results/enums/OpeningCategoryEnumTest.java b/backend/src/test/java/ca/bc/gov/restapi/results/oracle/enums/OpeningCategoryEnumTest.java
similarity index 83%
rename from backend/src/test/java/ca/bc/gov/restapi/results/enums/OpeningCategoryEnumTest.java
rename to backend/src/test/java/ca/bc/gov/restapi/results/oracle/enums/OpeningCategoryEnumTest.java
index 029c0f9d..b725e5e5 100644
--- a/backend/src/test/java/ca/bc/gov/restapi/results/enums/OpeningCategoryEnumTest.java
+++ b/backend/src/test/java/ca/bc/gov/restapi/results/oracle/enums/OpeningCategoryEnumTest.java
@@ -1,6 +1,5 @@
-package ca.bc.gov.restapi.results.enums;
+package ca.bc.gov.restapi.results.oracle.enums;
-import ca.bc.gov.restapi.results.oracle.enums.OpeningCategoryEnum;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
diff --git a/backend/src/test/java/ca/bc/gov/restapi/results/enums/OpeningStatusEnumTest.java b/backend/src/test/java/ca/bc/gov/restapi/results/oracle/enums/OpeningStatusEnumTest.java
similarity index 83%
rename from backend/src/test/java/ca/bc/gov/restapi/results/enums/OpeningStatusEnumTest.java
rename to backend/src/test/java/ca/bc/gov/restapi/results/oracle/enums/OpeningStatusEnumTest.java
index 4a48a358..ba0a8ca5 100644
--- a/backend/src/test/java/ca/bc/gov/restapi/results/enums/OpeningStatusEnumTest.java
+++ b/backend/src/test/java/ca/bc/gov/restapi/results/oracle/enums/OpeningStatusEnumTest.java
@@ -1,6 +1,5 @@
-package ca.bc.gov.restapi.results.enums;
+package ca.bc.gov.restapi.results.oracle.enums;
-import ca.bc.gov.restapi.results.oracle.enums.OpeningStatusEnum;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
diff --git a/backend/src/test/java/ca/bc/gov/restapi/results/postgres/endpoint/DashboardMetricsEndpointTest.java b/backend/src/test/java/ca/bc/gov/restapi/results/postgres/endpoint/DashboardMetricsEndpointTest.java
index d64315e0..4ee94da3 100644
--- a/backend/src/test/java/ca/bc/gov/restapi/results/postgres/endpoint/DashboardMetricsEndpointTest.java
+++ b/backend/src/test/java/ca/bc/gov/restapi/results/postgres/endpoint/DashboardMetricsEndpointTest.java
@@ -7,9 +7,14 @@
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+import ca.bc.gov.restapi.results.postgres.dto.DashboardFiltesDto;
+import ca.bc.gov.restapi.results.postgres.dto.FreeGrowingMilestonesDto;
+import ca.bc.gov.restapi.results.postgres.dto.MyRecentActionsRequestsDto;
import ca.bc.gov.restapi.results.postgres.dto.OpeningsPerYearDto;
-import ca.bc.gov.restapi.results.postgres.dto.OpeningsPerYearFiltersDto;
import ca.bc.gov.restapi.results.postgres.service.DashboardMetricsService;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@@ -31,7 +36,7 @@ class DashboardMetricsEndpointTest {
@Test
@DisplayName("Opening submission trends with no filters should succeed")
void getOpeningsSubmissionTrends_noFilters_shouldSucceed() throws Exception {
- OpeningsPerYearFiltersDto filtersDto = new OpeningsPerYearFiltersDto(null, null, null, null);
+ DashboardFiltesDto filtersDto = new DashboardFiltesDto(null, null, null, null, null);
OpeningsPerYearDto dto = new OpeningsPerYearDto(1, "Jan", 70);
when(dashboardMetricsService.getOpeningsSubmissionTrends(filtersDto)).thenReturn(List.of(dto));
@@ -53,7 +58,7 @@ void getOpeningsSubmissionTrends_noFilters_shouldSucceed() throws Exception {
@Test
@DisplayName("Opening submission trends with no data should succeed")
void getOpeningsSubmissionTrends_orgUnitFilter_shouldSucceed() throws Exception {
- OpeningsPerYearFiltersDto filtersDto = new OpeningsPerYearFiltersDto("DCR", null, null, null);
+ DashboardFiltesDto filtersDto = new DashboardFiltesDto("DCR", null, null, null, null);
when(dashboardMetricsService.getOpeningsSubmissionTrends(filtersDto)).thenReturn(List.of());
@@ -66,4 +71,126 @@ void getOpeningsSubmissionTrends_orgUnitFilter_shouldSucceed() throws Exception
.andExpect(status().isNoContent())
.andReturn();
}
+
+ @Test
+ @DisplayName("Free growing milestones test with no filters should succeed")
+ void getFreeGrowingMilestonesData_noFilters_shouldSucceed() throws Exception {
+ DashboardFiltesDto filtersDto = new DashboardFiltesDto(null, null, null, null, null);
+
+ FreeGrowingMilestonesDto milestonesDto =
+ new FreeGrowingMilestonesDto(0, "0 - 5 months", 25, new BigDecimal("100"));
+ when(dashboardMetricsService.getFreeGrowingMilestoneChartData(filtersDto))
+ .thenReturn(List.of(milestonesDto));
+
+ mockMvc
+ .perform(
+ get("/api/dashboard-metrics/free-growing-milestones")
+ .with(csrf().asHeader())
+ .header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
+ .accept(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andExpect(content().contentType(MediaType.APPLICATION_JSON))
+ .andExpect(jsonPath("$[0].index").value("0"))
+ .andExpect(jsonPath("$[0].label").value("0 - 5 months"))
+ .andExpect(jsonPath("$[0].amount").value("25"))
+ .andExpect(jsonPath("$[0].percentage").value(new BigDecimal("100")))
+ .andReturn();
+ }
+
+ @Test
+ @DisplayName("Free growing milestones test with client number filter should succeed")
+ void getFreeGrowingMilestonesData_clientNumberFilter_shouldSucceed() throws Exception {
+ List dtoList = new ArrayList<>();
+ dtoList.add(new FreeGrowingMilestonesDto(0, "0 - 5 months", 25, new BigDecimal("25")));
+ dtoList.add(new FreeGrowingMilestonesDto(1, "6 - 11 months", 25, new BigDecimal("25")));
+ dtoList.add(new FreeGrowingMilestonesDto(2, "12 - 17 months", 25, new BigDecimal("25")));
+ dtoList.add(new FreeGrowingMilestonesDto(3, "18 months", 25, new BigDecimal("25")));
+
+ DashboardFiltesDto filtersDto = new DashboardFiltesDto(null, null, null, null, "00012797");
+
+ when(dashboardMetricsService.getFreeGrowingMilestoneChartData(filtersDto)).thenReturn(dtoList);
+
+ mockMvc
+ .perform(
+ get("/api/dashboard-metrics/free-growing-milestones?clientNumber=00012797")
+ .with(csrf().asHeader())
+ .header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
+ .accept(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andExpect(content().contentType(MediaType.APPLICATION_JSON))
+ .andExpect(jsonPath("$[0].index").value("0"))
+ .andExpect(jsonPath("$[0].label").value("0 - 5 months"))
+ .andExpect(jsonPath("$[0].amount").value("25"))
+ .andExpect(jsonPath("$[0].percentage").value(new BigDecimal("25")))
+ .andExpect(jsonPath("$[1].index").value("1"))
+ .andExpect(jsonPath("$[1].label").value("6 - 11 months"))
+ .andExpect(jsonPath("$[1].amount").value("25"))
+ .andExpect(jsonPath("$[1].percentage").value(new BigDecimal("25")))
+ .andExpect(jsonPath("$[2].index").value("2"))
+ .andExpect(jsonPath("$[2].label").value("12 - 17 months"))
+ .andExpect(jsonPath("$[2].amount").value("25"))
+ .andExpect(jsonPath("$[2].percentage").value(new BigDecimal("25")))
+ .andExpect(jsonPath("$[3].index").value("3"))
+ .andExpect(jsonPath("$[3].label").value("18 months"))
+ .andExpect(jsonPath("$[3].amount").value("25"))
+ .andExpect(jsonPath("$[3].percentage").value(new BigDecimal("25")))
+ .andReturn();
+ }
+
+ @Test
+ @DisplayName("Free growing milestones test with no content should succeed")
+ void getFreeGrowingMilestonesData_noData_shouldSucceed() throws Exception {
+ DashboardFiltesDto filtersDto = new DashboardFiltesDto(null, null, null, null, "00012579");
+
+ when(dashboardMetricsService.getFreeGrowingMilestoneChartData(filtersDto))
+ .thenReturn(List.of());
+
+ mockMvc
+ .perform(
+ get("/api/dashboard-metrics/free-growing-milestones")
+ .with(csrf().asHeader())
+ .header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
+ .accept(MediaType.APPLICATION_JSON))
+ .andExpect(status().isNoContent())
+ .andReturn();
+ }
+
+ @Test
+ @DisplayName("User recent actions requests test happy path should succeed")
+ void getUserRecentOpeningsActions_happyPath_shouldSucceed() throws Exception {
+ MyRecentActionsRequestsDto actionDto =
+ new MyRecentActionsRequestsDto(
+ "Created", 48L, "PEN", "Pending", "2 minutes ago", LocalDateTime.now());
+ when(dashboardMetricsService.getUserRecentOpeningsActions()).thenReturn(List.of(actionDto));
+
+ mockMvc
+ .perform(
+ get("/api/dashboard-metrics/my-recent-actions/requests")
+ .with(csrf().asHeader())
+ .header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
+ .accept(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andExpect(content().contentType(MediaType.APPLICATION_JSON))
+ .andExpect(jsonPath("$[0].activityType").value("Created"))
+ .andExpect(jsonPath("$[0].openingId").value("48"))
+ .andExpect(jsonPath("$[0].statusCode").value("PEN"))
+ .andExpect(jsonPath("$[0].statusDescription").value("Pending"))
+ .andExpect(jsonPath("$[0].lastUpdatedLabel").value("2 minutes ago"))
+ .andReturn();
+ }
+
+ @Test
+ @DisplayName("User recent actions requests test no data should succeed")
+ void getUserRecentOpeningsActions_noData_shouldSucceed() throws Exception {
+ when(dashboardMetricsService.getUserRecentOpeningsActions()).thenReturn(List.of());
+
+ mockMvc
+ .perform(
+ get("/api/dashboard-metrics/my-recent-actions/requests")
+ .with(csrf().asHeader())
+ .header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
+ .accept(MediaType.APPLICATION_JSON))
+ .andExpect(status().isNoContent())
+ .andReturn();
+ }
}
diff --git a/backend/src/test/java/ca/bc/gov/restapi/results/postgres/endpoint/UserOpeningEndpointTest.java b/backend/src/test/java/ca/bc/gov/restapi/results/postgres/endpoint/UserOpeningEndpointTest.java
new file mode 100644
index 00000000..e5d450ce
--- /dev/null
+++ b/backend/src/test/java/ca/bc/gov/restapi/results/postgres/endpoint/UserOpeningEndpointTest.java
@@ -0,0 +1,81 @@
+package ca.bc.gov.restapi.results.postgres.endpoint;
+
+import static org.mockito.Mockito.when;
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import ca.bc.gov.restapi.results.postgres.dto.MyRecentActionsRequestsDto;
+import ca.bc.gov.restapi.results.postgres.service.UserOpeningService;
+import java.time.LocalDateTime;
+import java.util.List;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.MediaType;
+import org.springframework.security.test.context.support.WithMockUser;
+import org.springframework.test.web.servlet.MockMvc;
+
+@WebMvcTest(UserOpeningEndpoint.class)
+@WithMockUser
+class UserOpeningEndpointTest {
+
+ @Autowired private MockMvc mockMvc;
+
+ @MockBean private UserOpeningService userOpeningService;
+
+ @Test
+ @DisplayName("Get user tracked openings happy path should succeed")
+ void getUserTrackedOpenings_happyPath_shoudSucceed() throws Exception {
+ MyRecentActionsRequestsDto action =
+ new MyRecentActionsRequestsDto(
+ "Update", 123456L, "APP", "Approved", "2 minutes ago", LocalDateTime.now());
+ when(userOpeningService.getUserTrackedOpenings()).thenReturn(List.of(action));
+
+ mockMvc
+ .perform(
+ get("/api/user-openings/dashboard-track-openings")
+ .with(csrf().asHeader())
+ .header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
+ .accept(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andExpect(content().contentType(MediaType.APPLICATION_JSON))
+ .andExpect(jsonPath("$[0].activityType").value("Update"))
+ .andExpect(jsonPath("$[0].openingId").value("123456"))
+ .andExpect(jsonPath("$[0].statusCode").value("APP"))
+ .andExpect(jsonPath("$[0].statusDescription").value("Approved"))
+ .andExpect(jsonPath("$[0].lastUpdatedLabel").value("2 minutes ago"))
+ .andReturn();
+ }
+
+ @Test
+ @DisplayName("Get user tracked openings no data should succeed")
+ void getUserTrackedOpenings_noData_shoudSucceed() throws Exception {
+ when(userOpeningService.getUserTrackedOpenings()).thenReturn(List.of());
+
+ mockMvc
+ .perform(
+ get("/api/user-openings/dashboard-track-openings")
+ .with(csrf().asHeader())
+ .header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
+ .accept(MediaType.APPLICATION_JSON))
+ .andExpect(status().isNoContent())
+ .andReturn();
+ }
+
+ void saveUserOpening_happyPath_shoudSucceed() throws Exception {
+ //
+ }
+
+ void deleteUserOpening_happyPath_shoudSucceed() throws Exception {
+ //
+ }
+
+ void deleteUserOpening_notFound_shoudFail() throws Exception {
+ //
+ }
+}
diff --git a/backend/src/test/java/ca/bc/gov/restapi/results/postgres/repository/OpeningsLastYearRepositoryIntegrationTest.java b/backend/src/test/java/ca/bc/gov/restapi/results/postgres/repository/OpeningsLastYearRepositoryIntegrationTest.java
new file mode 100644
index 00000000..7e5fcfdc
--- /dev/null
+++ b/backend/src/test/java/ca/bc/gov/restapi/results/postgres/repository/OpeningsLastYearRepositoryIntegrationTest.java
@@ -0,0 +1,38 @@
+package ca.bc.gov.restapi.results.postgres.repository;
+
+import ca.bc.gov.restapi.results.postgres.entity.OpeningsLastYearEntity;
+import java.util.List;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
+import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace;
+import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
+import org.springframework.test.context.jdbc.Sql;
+
+@DataJpaTest
+@AutoConfigureTestDatabase(replace = Replace.NONE)
+@Sql(scripts = {"classpath:sql_scripts/OpeningsLastYearRepositoryIntegrationTest.sql"})
+class OpeningsLastYearRepositoryIntegrationTest {
+
+ @Autowired private OpeningsLastYearRepository openingsLastYearRepository;
+
+ @Test
+ @DisplayName("find all by Opening ID in List")
+ void findAllByOpeningIdInListTest() {
+ List idList = List.of(7012511L, 7012512L, 7012513L);
+ List openingList =
+ openingsLastYearRepository.findAllByOpeningIdInList(idList);
+
+ Assertions.assertFalse(openingList.isEmpty());
+ Assertions.assertEquals(3, openingList.size());
+
+ OpeningsLastYearEntity first = openingList.get(0);
+
+ Assertions.assertEquals(7012511L, first.getOpeningId());
+ Assertions.assertEquals("TEST", first.getUserId());
+ Assertions.assertEquals("APP", first.getStatus());
+ Assertions.assertEquals("DCR", first.getOrgUnitCode());
+ }
+}
diff --git a/backend/src/test/java/ca/bc/gov/restapi/results/postgres/repository/UserOpeningRepositoryIntegrationTest.java b/backend/src/test/java/ca/bc/gov/restapi/results/postgres/repository/UserOpeningRepositoryIntegrationTest.java
new file mode 100644
index 00000000..e644b25d
--- /dev/null
+++ b/backend/src/test/java/ca/bc/gov/restapi/results/postgres/repository/UserOpeningRepositoryIntegrationTest.java
@@ -0,0 +1,5 @@
+package ca.bc.gov.restapi.results.postgres.repository;
+
+class UserOpeningRepositoryIntegrationTest {
+
+}
diff --git a/backend/src/test/java/ca/bc/gov/restapi/results/postgres/service/DashboardMetricsServiceTest.java b/backend/src/test/java/ca/bc/gov/restapi/results/postgres/service/DashboardMetricsServiceTest.java
index 85203c09..839f6fdc 100644
--- a/backend/src/test/java/ca/bc/gov/restapi/results/postgres/service/DashboardMetricsServiceTest.java
+++ b/backend/src/test/java/ca/bc/gov/restapi/results/postgres/service/DashboardMetricsServiceTest.java
@@ -2,10 +2,16 @@
import static org.mockito.Mockito.when;
+import ca.bc.gov.restapi.results.common.security.LoggedUserService;
+import ca.bc.gov.restapi.results.postgres.dto.DashboardFiltesDto;
+import ca.bc.gov.restapi.results.postgres.dto.FreeGrowingMilestonesDto;
+import ca.bc.gov.restapi.results.postgres.dto.MyRecentActionsRequestsDto;
import ca.bc.gov.restapi.results.postgres.dto.OpeningsPerYearDto;
-import ca.bc.gov.restapi.results.postgres.dto.OpeningsPerYearFiltersDto;
+import ca.bc.gov.restapi.results.postgres.entity.OpeningsActivityEntity;
import ca.bc.gov.restapi.results.postgres.entity.OpeningsLastYearEntity;
+import ca.bc.gov.restapi.results.postgres.repository.OpeningsActivityRepository;
import ca.bc.gov.restapi.results.postgres.repository.OpeningsLastYearRepository;
+import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import org.junit.jupiter.api.Assertions;
@@ -22,26 +28,35 @@ class DashboardMetricsServiceTest {
@Mock OpeningsLastYearRepository openingsLastYearRepository;
+ @Mock OpeningsActivityRepository openingsActivityRepository;
+
+ @Mock LoggedUserService loggedUserService;
+
private DashboardMetricsService dashboardMetricsService;
private static final Sort SORT = Sort.by("entryTimestamp").ascending();
+ private static final Sort SORT_BY_ID = Sort.by("openingId").ascending();
+
private List mockOpeningsEntityList() {
LocalDateTime entryTimestamp = LocalDateTime.now();
OpeningsLastYearEntity entity = new OpeningsLastYearEntity();
- entity.setOpeningId("123456");
+ entity.setOpeningId(123456L);
entity.setUserId("userId");
entity.setEntryTimestamp(entryTimestamp);
entity.setUpdateTimestamp(entryTimestamp);
entity.setStatus("APP");
entity.setOrgUnitCode("DCR");
+ entity.setClientNumber("00012797");
return List.of(entity);
}
@BeforeEach
void setup() {
- dashboardMetricsService = new DashboardMetricsService(openingsLastYearRepository);
+ dashboardMetricsService =
+ new DashboardMetricsService(
+ openingsLastYearRepository, openingsActivityRepository, loggedUserService);
}
@Test
@@ -51,7 +66,7 @@ void getOpeningsSubmissionTrends_noFilters_shouldSucceed() throws Exception {
List entities = mockOpeningsEntityList();
when(openingsLastYearRepository.findAll(SORT)).thenReturn(entities);
- OpeningsPerYearFiltersDto filtersDto = new OpeningsPerYearFiltersDto(null, null, null, null);
+ DashboardFiltesDto filtersDto = new DashboardFiltesDto(null, null, null, null, null);
List list = dashboardMetricsService.getOpeningsSubmissionTrends(filtersDto);
String monthName = now.getMonth().name().toLowerCase();
@@ -71,7 +86,7 @@ void getOpeningsSubmissionTrends_orgUnitFilter_shouldSucceed() throws Exception
List entities = mockOpeningsEntityList();
when(openingsLastYearRepository.findAll(SORT)).thenReturn(entities);
- OpeningsPerYearFiltersDto filtersDto = new OpeningsPerYearFiltersDto("AAA", null, null, null);
+ DashboardFiltesDto filtersDto = new DashboardFiltesDto("AAA", null, null, null, null);
List list = dashboardMetricsService.getOpeningsSubmissionTrends(filtersDto);
String monthName = now.getMonth().name().toLowerCase();
@@ -91,7 +106,7 @@ void getOpeningsSubmissionTrends_statusFilter_shouldSucceed() {
List entities = mockOpeningsEntityList();
when(openingsLastYearRepository.findAll(SORT)).thenReturn(entities);
- OpeningsPerYearFiltersDto filtersDto = new OpeningsPerYearFiltersDto(null, "APP", null, null);
+ DashboardFiltesDto filtersDto = new DashboardFiltesDto(null, "APP", null, null, null);
List list = dashboardMetricsService.getOpeningsSubmissionTrends(filtersDto);
String monthName = now.getMonth().name().toLowerCase();
@@ -105,17 +120,17 @@ void getOpeningsSubmissionTrends_statusFilter_shouldSucceed() {
}
@Test
- @DisplayName("Opening submission trends with Status filter should succeed")
+ @DisplayName("Opening submission trends with Dates filter should succeed")
void getOpeningsSubmissionTrends_datesFilter_shouldSucceed() {
- LocalDateTime now = LocalDateTime.now();
List entities = mockOpeningsEntityList();
when(openingsLastYearRepository.findAll(SORT)).thenReturn(entities);
+ LocalDateTime now = LocalDateTime.now();
LocalDateTime oneMonthBefore = now.minusMonths(1L);
LocalDateTime oneMonthLater = now.plusMonths(1L);
- OpeningsPerYearFiltersDto filtersDto =
- new OpeningsPerYearFiltersDto(null, null, oneMonthBefore, oneMonthLater);
+ DashboardFiltesDto filtersDto =
+ new DashboardFiltesDto(null, null, oneMonthBefore, oneMonthLater, null);
List list = dashboardMetricsService.getOpeningsSubmissionTrends(filtersDto);
String monthName = now.getMonth().name().toLowerCase();
@@ -127,4 +142,193 @@ void getOpeningsSubmissionTrends_datesFilter_shouldSucceed() {
Assertions.assertEquals(monthName, list.get(0).monthName());
Assertions.assertEquals(1, list.get(0).amount());
}
+
+ @Test
+ @DisplayName("Free growing milestones without filters should succeed")
+ void getFreeGrowingMilestoneChartData_noFilters_shouldSucceed() {
+ List entities = mockOpeningsEntityList();
+ when(openingsLastYearRepository.findAll(SORT_BY_ID)).thenReturn(entities);
+
+ DashboardFiltesDto filtersDto = new DashboardFiltesDto(null, null, null, null, null);
+ List list =
+ dashboardMetricsService.getFreeGrowingMilestoneChartData(filtersDto);
+
+ Assertions.assertFalse(list.isEmpty());
+ Assertions.assertEquals(4, list.size());
+ Assertions.assertEquals(0, list.get(0).index());
+ Assertions.assertEquals("0 - 5 months", list.get(0).label());
+ Assertions.assertEquals(1, list.get(0).amount());
+ Assertions.assertEquals(new BigDecimal("100.00"), list.get(0).percentage());
+
+ Assertions.assertEquals(1, list.get(1).index());
+ Assertions.assertEquals("6 - 11 months", list.get(1).label());
+ Assertions.assertEquals(0, list.get(1).amount());
+ Assertions.assertEquals(new BigDecimal("0.00"), list.get(1).percentage());
+
+ Assertions.assertEquals(2, list.get(2).index());
+ Assertions.assertEquals("12 - 17 months", list.get(2).label());
+ Assertions.assertEquals(0, list.get(2).amount());
+ Assertions.assertEquals(new BigDecimal("0.00"), list.get(2).percentage());
+
+ Assertions.assertEquals(3, list.get(3).index());
+ Assertions.assertEquals("18 months", list.get(3).label());
+ Assertions.assertEquals(0, list.get(3).amount());
+ Assertions.assertEquals(new BigDecimal("0.00"), list.get(3).percentage());
+ }
+
+ @Test
+ @DisplayName("Free growing milestones with org unit filter should succeed")
+ void getFreeGrowingMilestoneChartData_orgUnitFilter_shouldSucceed() {
+ List entities = mockOpeningsEntityList();
+ when(openingsLastYearRepository.findAll(SORT_BY_ID)).thenReturn(entities);
+
+ DashboardFiltesDto filtersDto = new DashboardFiltesDto("AAA", null, null, null, null);
+ List list =
+ dashboardMetricsService.getFreeGrowingMilestoneChartData(filtersDto);
+
+ Assertions.assertFalse(list.isEmpty());
+ Assertions.assertEquals(4, list.size());
+ Assertions.assertEquals(0, list.get(0).index());
+ Assertions.assertEquals("0 - 5 months", list.get(0).label());
+ Assertions.assertEquals(0, list.get(0).amount());
+ Assertions.assertEquals(BigDecimal.ZERO, list.get(0).percentage());
+
+ Assertions.assertEquals(1, list.get(1).index());
+ Assertions.assertEquals("6 - 11 months", list.get(1).label());
+ Assertions.assertEquals(0, list.get(1).amount());
+ Assertions.assertEquals(BigDecimal.ZERO, list.get(1).percentage());
+
+ Assertions.assertEquals(2, list.get(2).index());
+ Assertions.assertEquals("12 - 17 months", list.get(2).label());
+ Assertions.assertEquals(0, list.get(2).amount());
+ Assertions.assertEquals(BigDecimal.ZERO, list.get(2).percentage());
+
+ Assertions.assertEquals(3, list.get(3).index());
+ Assertions.assertEquals("18 months", list.get(3).label());
+ Assertions.assertEquals(0, list.get(3).amount());
+ Assertions.assertEquals(BigDecimal.ZERO, list.get(3).percentage());
+ }
+
+ @Test
+ @DisplayName("Free growing milestones with client number filter should succeed")
+ void getFreeGrowingMilestoneChartData_clientNumberFilter_shouldSucceed() {
+ List entities = mockOpeningsEntityList();
+ when(openingsLastYearRepository.findAll(SORT_BY_ID)).thenReturn(entities);
+
+ DashboardFiltesDto filtersDto = new DashboardFiltesDto(null, null, null, null, "00011254");
+ List list =
+ dashboardMetricsService.getFreeGrowingMilestoneChartData(filtersDto);
+
+ Assertions.assertFalse(list.isEmpty());
+ Assertions.assertEquals(4, list.size());
+ Assertions.assertEquals(0, list.get(0).index());
+ Assertions.assertEquals("0 - 5 months", list.get(0).label());
+ Assertions.assertEquals(0, list.get(0).amount());
+ Assertions.assertEquals(BigDecimal.ZERO, list.get(0).percentage());
+
+ Assertions.assertEquals(1, list.get(1).index());
+ Assertions.assertEquals("6 - 11 months", list.get(1).label());
+ Assertions.assertEquals(0, list.get(1).amount());
+ Assertions.assertEquals(BigDecimal.ZERO, list.get(1).percentage());
+
+ Assertions.assertEquals(2, list.get(2).index());
+ Assertions.assertEquals("12 - 17 months", list.get(2).label());
+ Assertions.assertEquals(0, list.get(2).amount());
+ Assertions.assertEquals(BigDecimal.ZERO, list.get(2).percentage());
+
+ Assertions.assertEquals(3, list.get(3).index());
+ Assertions.assertEquals("18 months", list.get(3).label());
+ Assertions.assertEquals(0, list.get(3).amount());
+ Assertions.assertEquals(BigDecimal.ZERO, list.get(3).percentage());
+ }
+
+ @Test
+ @DisplayName("Free growing milestones with dates filter should succeed")
+ void getFreeGrowingMilestoneChartData_datesFilter_shouldSucceed() {
+ List entities = mockOpeningsEntityList();
+ when(openingsLastYearRepository.findAll(SORT_BY_ID)).thenReturn(entities);
+
+ LocalDateTime now = LocalDateTime.now();
+ LocalDateTime oneMonthBefore = now.minusMonths(1L);
+ LocalDateTime oneMonthLater = now.plusMonths(1L);
+
+ DashboardFiltesDto filtersDto =
+ new DashboardFiltesDto(null, null, oneMonthBefore, oneMonthLater, null);
+ List list =
+ dashboardMetricsService.getFreeGrowingMilestoneChartData(filtersDto);
+
+ Assertions.assertFalse(list.isEmpty());
+ Assertions.assertEquals(4, list.size());
+ Assertions.assertEquals(0, list.get(0).index());
+ Assertions.assertEquals("0 - 5 months", list.get(0).label());
+ Assertions.assertEquals(1, list.get(0).amount());
+ Assertions.assertEquals(new BigDecimal("100.00"), list.get(0).percentage());
+
+ Assertions.assertEquals(1, list.get(1).index());
+ Assertions.assertEquals("6 - 11 months", list.get(1).label());
+ Assertions.assertEquals(0, list.get(1).amount());
+ Assertions.assertEquals(new BigDecimal("0.00"), list.get(1).percentage());
+
+ Assertions.assertEquals(2, list.get(2).index());
+ Assertions.assertEquals("12 - 17 months", list.get(2).label());
+ Assertions.assertEquals(0, list.get(2).amount());
+ Assertions.assertEquals(new BigDecimal("0.00"), list.get(2).percentage());
+
+ Assertions.assertEquals(3, list.get(3).index());
+ Assertions.assertEquals("18 months", list.get(3).label());
+ Assertions.assertEquals(0, list.get(3).amount());
+ Assertions.assertEquals(new BigDecimal("0.00"), list.get(3).percentage());
+ }
+
+ @Test
+ @DisplayName("My recent activities request table data test happy path should succeed")
+ void getUserRecentOpeningsActions_happyPath_shouldSucceed() {
+ String userId = "TEST";
+
+ when(loggedUserService.getLoggedUserId()).thenReturn(userId);
+
+ OpeningsActivityEntity activity = new OpeningsActivityEntity();
+ activity.setOpeningId(112233L);
+ // If you're here looking for more codes and descriptions, you may want to take a look on
+ // the jira issue: https://apps.nrs.gov.bc.ca/int/jira/browse/SILVA-362 look for the comment
+ // with the 'fire' emoji, made on 20/Mar/24 3:03 PM
+ activity.setActivityTypeCode("UPD");
+ activity.setActivityTypeDesc("Update");
+ activity.setStatusCode("APP");
+ activity.setStatusDesc("Approved");
+ activity.setLastUpdated(LocalDateTime.now().minusHours(2));
+ activity.setEntryUserid(userId);
+
+ Sort sort = Sort.by("lastUpdated").descending();
+ when(openingsActivityRepository.findAllByEntryUserid(userId, sort))
+ .thenReturn(List.of(activity));
+
+ List dtoList =
+ dashboardMetricsService.getUserRecentOpeningsActions();
+
+ Assertions.assertFalse(dtoList.isEmpty());
+ Assertions.assertEquals(1, dtoList.size());
+ Assertions.assertEquals("Update", dtoList.get(0).activityType());
+ Assertions.assertEquals(112233L, dtoList.get(0).openingId());
+ Assertions.assertEquals("APP", dtoList.get(0).statusCode());
+ Assertions.assertEquals("Approved", dtoList.get(0).statusDescription());
+ Assertions.assertEquals("2 hours ago", dtoList.get(0).lastUpdatedLabel());
+ }
+
+ @Test
+ @DisplayName("My recent activities request table data test no data should succeed")
+ void getUserRecentOpeningsActions_noData_shouldSucceed() {
+ String userId = "TEST";
+
+ when(loggedUserService.getLoggedUserId()).thenReturn(userId);
+
+ Sort sort = Sort.by("lastUpdated").descending();
+ when(openingsActivityRepository.findAllByEntryUserid(userId, sort))
+ .thenReturn(List.of());
+
+ List dtoList =
+ dashboardMetricsService.getUserRecentOpeningsActions();
+
+ Assertions.assertTrue(dtoList.isEmpty());
+ }
}
diff --git a/backend/src/test/java/ca/bc/gov/restapi/results/postgres/service/UserOpeningServiceTest.java b/backend/src/test/java/ca/bc/gov/restapi/results/postgres/service/UserOpeningServiceTest.java
new file mode 100644
index 00000000..7e60b157
--- /dev/null
+++ b/backend/src/test/java/ca/bc/gov/restapi/results/postgres/service/UserOpeningServiceTest.java
@@ -0,0 +1,125 @@
+package ca.bc.gov.restapi.results.postgres.service;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.when;
+
+import ca.bc.gov.restapi.results.common.exception.UserOpeningNotFoundException;
+import ca.bc.gov.restapi.results.common.security.LoggedUserService;
+import ca.bc.gov.restapi.results.postgres.dto.MyRecentActionsRequestsDto;
+import ca.bc.gov.restapi.results.postgres.entity.OpeningsActivityEntity;
+import ca.bc.gov.restapi.results.postgres.entity.UserOpeningEntity;
+import ca.bc.gov.restapi.results.postgres.repository.OpeningsActivityRepository;
+import ca.bc.gov.restapi.results.postgres.repository.UserOpeningRepository;
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Optional;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class UserOpeningServiceTest {
+
+ @Mock LoggedUserService loggedUserService;
+
+ @Mock UserOpeningRepository userOpeningRepository;
+
+ @Mock OpeningsActivityRepository openingsActivityRepository;
+
+ private UserOpeningService userOpeningService;
+
+ private static final String USER_ID = "TEST";
+
+ @BeforeEach
+ void setup() {
+ this.userOpeningService =
+ new UserOpeningService(
+ loggedUserService, userOpeningRepository, openingsActivityRepository);
+ }
+
+ @Test
+ @DisplayName("Get user tracked openings happy path should succeed")
+ void getUserTrackedOpenings_happyPath_shouldSucceed() {
+ when(loggedUserService.getLoggedUserId()).thenReturn(USER_ID);
+
+ UserOpeningEntity entity = new UserOpeningEntity();
+ entity.setUserId(USER_ID);
+ entity.setOpeningId(223344L);
+
+ when(userOpeningRepository.findAllByUserId(USER_ID)).thenReturn(List.of(entity));
+
+ LocalDateTime now = LocalDateTime.now().minusMinutes(2);
+ OpeningsActivityEntity openingEntity = new OpeningsActivityEntity();
+ openingEntity.setOpeningId(entity.getOpeningId());
+ openingEntity.setActivityTypeCode("UPD");
+ openingEntity.setActivityTypeDesc("Update");
+ openingEntity.setStatusCode("APP");
+ openingEntity.setStatusDesc("Approved");
+ openingEntity.setLastUpdated(now);
+ openingEntity.setEntryUserid(USER_ID);
+
+ when(openingsActivityRepository.findAllById(List.of(223344L)))
+ .thenReturn(List.of(openingEntity));
+
+ List openings = userOpeningService.getUserTrackedOpenings();
+
+ Assertions.assertFalse(openings.isEmpty());
+ Assertions.assertEquals("Update", openings.get(0).activityType());
+ Assertions.assertEquals(223344L, openings.get(0).openingId());
+ Assertions.assertEquals("APP", openings.get(0).statusCode());
+ Assertions.assertEquals("Approved", openings.get(0).statusDescription());
+ Assertions.assertEquals("2 minutes ago", openings.get(0).lastUpdatedLabel());
+ }
+
+ @Test
+ @DisplayName("Get user tracked openings no data should succeed")
+ void getUserTrackedOpenings_noData_shouldSucceed() {
+ when(loggedUserService.getLoggedUserId()).thenReturn(USER_ID);
+
+ when(userOpeningRepository.findAllByUserId(USER_ID)).thenReturn(List.of());
+
+ List openings = userOpeningService.getUserTrackedOpenings();
+
+ Assertions.assertTrue(openings.isEmpty());
+ }
+
+ @Test
+ @DisplayName("Save opening to user happy path shoudl succeed")
+ void saveOpeningToUser_happyPath_shouldSucceed() {
+ when(loggedUserService.getLoggedUserId()).thenReturn(USER_ID);
+ when(userOpeningRepository.saveAndFlush(any())).thenReturn(new UserOpeningEntity());
+ userOpeningService.saveOpeningToUser(112233L);
+ }
+
+ @Test
+ @DisplayName("Delete opening from user's favourite happy path should succeed")
+ void deleteOpeningFromUserFavourite_happyPath_shouldSucceed() {
+ when(loggedUserService.getLoggedUserId()).thenReturn(USER_ID);
+
+ UserOpeningEntity userEntity = new UserOpeningEntity();
+ when(userOpeningRepository.findById(any())).thenReturn(Optional.of(userEntity));
+
+ doNothing().when(userOpeningRepository).delete(any());
+ doNothing().when(userOpeningRepository).flush();
+
+ userOpeningService.deleteOpeningFromUserFavourite(112233L);
+ }
+
+ @Test
+ @DisplayName("Delete opening from user's favourite not found should fail")
+ void deleteOpeningFromUserFavourite_notFound_shouldFail() {
+ when(loggedUserService.getLoggedUserId()).thenReturn(USER_ID);
+ when(userOpeningRepository.findById(any())).thenReturn(Optional.empty());
+
+ Assertions.assertThrows(
+ UserOpeningNotFoundException.class,
+ () -> {
+ userOpeningService.deleteOpeningFromUserFavourite(112233L);
+ });
+ }
+}
diff --git a/backend/src/test/resources/sql_scripts/OpeningsLastYearRepositoryIntegrationTest.sql b/backend/src/test/resources/sql_scripts/OpeningsLastYearRepositoryIntegrationTest.sql
new file mode 100644
index 00000000..23ddd360
--- /dev/null
+++ b/backend/src/test/resources/sql_scripts/OpeningsLastYearRepositoryIntegrationTest.sql
@@ -0,0 +1,14 @@
+insert into openings_last_year (
+ opening_id
+ ,opening_entry_userid
+ ,entry_timestamp
+ ,update_timestamp
+ ,status_code
+ ,org_unit_code
+ ,client_number
+) values
+ (7012511, 'TEST', '2024-04-05', '2024-04-08', 'APP', 'DCR', '00012797'),
+ (7012512, 'TEST', '2024-02-03', '2024-03-08', 'APP', 'DCR', '00012797'),
+ (7012513, 'TEST', '2024-03-04', '2024-04-08', 'APP', 'DCR', '00012797'),
+ (7012514, 'TEST', '2024-01-15', '2024-02-15', 'APP', 'DCR', '00012797'),
+ (7012515, 'TEST', '2024-02-18', '2024-02-25', 'APP', 'DCR', '00012797');