diff --git a/app/build.gradle b/app/build.gradle index ad259448..27f1ea6b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -91,6 +91,9 @@ micronaut { incremental(true) annotations("app.*") } + testResources { + enabled = System.getenv("MICRONAUT_TEST_RESOURCES_ENABLED") == "false" ? false : true + } } tasks.withType(JavaCompile) { diff --git a/app/src/main/java/app/JurisdictionAdminController.java b/app/src/main/java/app/JurisdictionAdminController.java index 592d2aa1..103d9536 100644 --- a/app/src/main/java/app/JurisdictionAdminController.java +++ b/app/src/main/java/app/JurisdictionAdminController.java @@ -42,6 +42,11 @@ import java.net.MalformedURLException; import java.util.List; +import app.dto.servicerequest.ServiceRequestRemovalSuggestionDTO; +import app.model.servicerequest.ServiceRequestRemovalSuggestion; +import io.micronaut.data.model.Page; +import io.micronaut.data.model.Pageable; + import static app.security.Permission.*; @Controller("/api/jurisdiction-admin") @@ -56,6 +61,23 @@ public JurisdictionAdminController(ServiceService serviceService, ServiceRequest this.serviceRequestService = serviceRequestService; } + @Get(uris = { "/requests/removal-suggestions{?jurisdiction_id}", "/requests/removal-suggestions.json{?jurisdiction_id}" }) + @ExecuteOn(TaskExecutors.IO) + @RequiresPermissions({LIBRE311_ADMIN_VIEW_SYSTEM, LIBRE311_ADMIN_VIEW_TENANT, LIBRE311_ADMIN_VIEW_SUBTENANT}) + public Page getRemovalSuggestions(@Valid Pageable pageable, + @Nullable @QueryValue("jurisdiction_id") String jurisdiction_id) { + return serviceRequestService.getRemovalSuggestions(jurisdiction_id, pageable); + } + + @Delete(uris = { "/requests/removal-suggestions/{id}{?jurisdiction_id}", "/requests/removal-suggestions/{id}.json{?jurisdiction_id}" }) + @ExecuteOn(TaskExecutors.IO) + @RequiresPermissions({LIBRE311_ADMIN_EDIT_SYSTEM, LIBRE311_ADMIN_EDIT_TENANT, LIBRE311_ADMIN_EDIT_SUBTENANT}) + public HttpResponse deleteRemovalSuggestion(Long id, + @Nullable @QueryValue("jurisdiction_id") String jurisdiction_id) { + serviceRequestService.deleteRemovalSuggestion(id, jurisdiction_id); + return HttpResponse.ok(); + } + @Post(uris = { "/services{?jurisdiction_id}", "/services.json{?jurisdiction_id}" }) @ExecuteOn(TaskExecutors.IO) @RequiresPermissions({LIBRE311_ADMIN_EDIT_SYSTEM, LIBRE311_ADMIN_EDIT_TENANT, LIBRE311_ADMIN_EDIT_SUBTENANT}) diff --git a/app/src/main/java/app/RootController.java b/app/src/main/java/app/RootController.java index 58d4cc00..71df7b46 100644 --- a/app/src/main/java/app/RootController.java +++ b/app/src/main/java/app/RootController.java @@ -164,6 +164,19 @@ public String createServiceRequestXml(HttpRequest request, return xmlMapper.writeValueAsString(serviceRequestList); } + @Post(uris = {"/requests/{serviceRequestId}/removal-suggestions{?jurisdiction_id}", "/requests/{serviceRequestId}/removal-suggestions.json{?jurisdiction_id}"}) + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @ExecuteOn(TaskExecutors.IO) + public HttpResponse createServiceRequestRemovalSuggestion( + @PathVariable Long serviceRequestId, + @Valid @Body PostRequestServiceRequestRemovalSuggestionDTO requestDTO, + @Nullable @QueryValue("jurisdiction_id") String jurisdiction_id) { + + serviceRequestService.createRemovalSuggestion(serviceRequestId, jurisdiction_id, requestDTO); + return HttpResponse.ok(); + } + @Get(uris = {"/requests{?jurisdiction_id}", "/requests.json{?jurisdiction_id}"}) @Produces(MediaType.APPLICATION_JSON) @ExecuteOn(TaskExecutors.IO) diff --git a/app/src/main/java/app/dto/servicerequest/PostRequestServiceRequestRemovalSuggestionDTO.java b/app/src/main/java/app/dto/servicerequest/PostRequestServiceRequestRemovalSuggestionDTO.java new file mode 100644 index 00000000..7260486a --- /dev/null +++ b/app/src/main/java/app/dto/servicerequest/PostRequestServiceRequestRemovalSuggestionDTO.java @@ -0,0 +1,75 @@ +package app.dto.servicerequest; + +import io.micronaut.core.annotation.Introspected; +import io.micronaut.core.annotation.Nullable; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import com.fasterxml.jackson.annotation.JsonProperty; + +@Introspected +public class PostRequestServiceRequestRemovalSuggestionDTO { + + @NotNull + @Email(regexp = "^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$") + private String email; + + @Nullable + private String name; + + @Nullable + private String phone; + + @NotNull + @NotBlank + private String reason; + + @NotBlank + @JsonProperty("g_recaptcha_response") + private String gRecaptchaResponse; + + public PostRequestServiceRequestRemovalSuggestionDTO() { + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + @Nullable + public String getName() { + return name; + } + + public void setName(@Nullable String name) { + this.name = name; + } + + @Nullable + public String getPhone() { + return phone; + } + + public void setPhone(@Nullable String phone) { + this.phone = phone; + } + + public String getReason() { + return reason; + } + + public void setReason(String reason) { + this.reason = reason; + } + + public String getgRecaptchaResponse() { + return gRecaptchaResponse; + } + + public void setgRecaptchaResponse(String gRecaptchaResponse) { + this.gRecaptchaResponse = gRecaptchaResponse; + } +} diff --git a/app/src/main/java/app/dto/servicerequest/ServiceRequestDTO.java b/app/src/main/java/app/dto/servicerequest/ServiceRequestDTO.java index cf20adc1..cf42263e 100644 --- a/app/src/main/java/app/dto/servicerequest/ServiceRequestDTO.java +++ b/app/src/main/java/app/dto/servicerequest/ServiceRequestDTO.java @@ -89,6 +89,9 @@ public class ServiceRequestDTO implements ServiceRequestResponseDTO { @JsonProperty("selected_values") private List selectedValues; + @JsonProperty("removal_suggestions_count") + private Long removalSuggestionsCount = 0L; + public ServiceRequestDTO() { } @@ -273,4 +276,12 @@ public List getSelectedValues() { public void setSelectedValues(List selectedValues) { this.selectedValues = selectedValues; } + + public Long getRemovalSuggestionsCount() { + return removalSuggestionsCount; + } + + public void setRemovalSuggestionsCount(Long removalSuggestionsCount) { + this.removalSuggestionsCount = removalSuggestionsCount; + } } diff --git a/app/src/main/java/app/dto/servicerequest/ServiceRequestRemovalSuggestionDTO.java b/app/src/main/java/app/dto/servicerequest/ServiceRequestRemovalSuggestionDTO.java new file mode 100644 index 00000000..819a00fc --- /dev/null +++ b/app/src/main/java/app/dto/servicerequest/ServiceRequestRemovalSuggestionDTO.java @@ -0,0 +1,86 @@ +package app.dto.servicerequest; + +import app.model.servicerequest.ServiceRequest; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.micronaut.core.annotation.Introspected; +import io.micronaut.core.annotation.Nullable; +import java.time.Instant; + +@Introspected +public class ServiceRequestRemovalSuggestionDTO { + + private Long id; + @JsonProperty("service_request_id") + private Long serviceRequestId; + private String email; + @Nullable + private String name; + @Nullable + private String phone; + private String reason; + @JsonProperty("date_created") + @JsonFormat(shape = JsonFormat.Shape.STRING) + private Instant dateCreated; + + public ServiceRequestRemovalSuggestionDTO() { + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getServiceRequestId() { + return serviceRequestId; + } + + public void setServiceRequestId(Long serviceRequestId) { + this.serviceRequestId = serviceRequestId; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + @Nullable + public String getName() { + return name; + } + + public void setName(@Nullable String name) { + this.name = name; + } + + @Nullable + public String getPhone() { + return phone; + } + + public void setPhone(@Nullable String phone) { + this.phone = phone; + } + + public String getReason() { + return reason; + } + + public void setReason(String reason) { + this.reason = reason; + } + + public Instant getDateCreated() { + return dateCreated; + } + + public void setDateCreated(Instant dateCreated) { + this.dateCreated = dateCreated; + } +} diff --git a/app/src/main/java/app/model/servicerequest/ServiceRequest.java b/app/src/main/java/app/model/servicerequest/ServiceRequest.java index ca5cf2f6..12a5887d 100644 --- a/app/src/main/java/app/model/servicerequest/ServiceRequest.java +++ b/app/src/main/java/app/model/servicerequest/ServiceRequest.java @@ -20,6 +20,7 @@ import io.micronaut.data.annotation.DateCreated; import io.micronaut.data.annotation.DateUpdated; +import io.micronaut.data.annotation.Where; import jakarta.persistence.*; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotNull; @@ -30,6 +31,7 @@ @Entity @Table(name = "service_requests") +@Where(value = "@.deleted = false") public class ServiceRequest { @Id diff --git a/app/src/main/java/app/model/servicerequest/ServiceRequestRemovalSuggestion.java b/app/src/main/java/app/model/servicerequest/ServiceRequestRemovalSuggestion.java new file mode 100644 index 00000000..b0f146f0 --- /dev/null +++ b/app/src/main/java/app/model/servicerequest/ServiceRequestRemovalSuggestion.java @@ -0,0 +1,123 @@ +package app.model.servicerequest; + +import io.micronaut.core.annotation.Nullable; +import io.micronaut.data.annotation.DateCreated; +import io.micronaut.data.annotation.Where; +import jakarta.persistence.*; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotNull; +import java.time.Instant; + +@Entity +@Table(name = "service_request_removal_suggestions") +@Where(value = "@.deleted = false") +public class ServiceRequestRemovalSuggestion { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotNull + @Column(name = "service_request_id") + private Long serviceRequestId; + + @NotNull + @Column(name = "jurisdiction_id") + private String jurisdictionId; + + @NotNull + @Email(regexp = "^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$") + private String email; + + @Nullable + private String name; + + @Nullable + private String phone; + + @NotNull + @Column(columnDefinition = "TEXT") + private String reason; + + @DateCreated + private Instant dateCreated; + + private boolean deleted; + + public ServiceRequestRemovalSuggestion() { + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getServiceRequestId() { + return serviceRequestId; + } + + public void setServiceRequestId(Long serviceRequestId) { + this.serviceRequestId = serviceRequestId; + } + + public String getJurisdictionId() { + return jurisdictionId; + } + + public void setJurisdictionId(String jurisdictionId) { + this.jurisdictionId = jurisdictionId; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + @Nullable + public String getName() { + return name; + } + + public void setName(@Nullable String name) { + this.name = name; + } + + @Nullable + public String getPhone() { + return phone; + } + + public void setPhone(@Nullable String phone) { + this.phone = phone; + } + + public String getReason() { + return reason; + } + + public void setReason(String reason) { + this.reason = reason; + } + + public Instant getDateCreated() { + return dateCreated; + } + + public void setDateCreated(Instant dateCreated) { + this.dateCreated = dateCreated; + } + + public boolean isDeleted() { + return deleted; + } + + public void setDeleted(boolean deleted) { + this.deleted = deleted; + } +} \ No newline at end of file diff --git a/app/src/main/java/app/model/servicerequest/ServiceRequestRemovalSuggestionCount.java b/app/src/main/java/app/model/servicerequest/ServiceRequestRemovalSuggestionCount.java new file mode 100644 index 00000000..ee27e12c --- /dev/null +++ b/app/src/main/java/app/model/servicerequest/ServiceRequestRemovalSuggestionCount.java @@ -0,0 +1,32 @@ +package app.model.servicerequest; + +import io.micronaut.core.annotation.Introspected; + +@Introspected +public class ServiceRequestRemovalSuggestionCount { + private Long serviceRequestId; + private Long count; + + public ServiceRequestRemovalSuggestionCount() {} + + public ServiceRequestRemovalSuggestionCount(Long serviceRequestId, Long count) { + this.serviceRequestId = serviceRequestId; + this.count = count; + } + + public Long getServiceRequestId() { + return serviceRequestId; + } + + public void setServiceRequestId(Long serviceRequestId) { + this.serviceRequestId = serviceRequestId; + } + + public Long getCount() { + return count; + } + + public void setCount(Long count) { + this.count = count; + } +} diff --git a/app/src/main/java/app/model/servicerequest/ServiceRequestRemovalSuggestionRepository.java b/app/src/main/java/app/model/servicerequest/ServiceRequestRemovalSuggestionRepository.java new file mode 100644 index 00000000..0e9da665 --- /dev/null +++ b/app/src/main/java/app/model/servicerequest/ServiceRequestRemovalSuggestionRepository.java @@ -0,0 +1,24 @@ +package app.model.servicerequest; + +import app.dto.servicerequest.ServiceRequestRemovalSuggestionDTO; +import io.micronaut.data.annotation.Query; +import io.micronaut.data.annotation.Repository; +import io.micronaut.data.model.Page; +import io.micronaut.data.model.Pageable; +import io.micronaut.data.repository.CrudRepository; +import java.util.List; +import java.util.Map; + +@Repository +public interface ServiceRequestRemovalSuggestionRepository extends CrudRepository { + Page findAllByJurisdictionId(String jurisdictionId, Pageable pageable); + + @Query("update ServiceRequestRemovalSuggestion s set s.deleted = true where s.serviceRequestId = :serviceRequestId and s.jurisdictionId = :jurisdictionId") + void deleteByServiceRequestIdAndJurisdictionId(Long serviceRequestId, String jurisdictionId); + + @Query("update ServiceRequestRemovalSuggestion s set s.deleted = true where s.id = :id and s.jurisdictionId = :jurisdictionId") + void softDelete(Long id, String jurisdictionId); + + @Query("SELECT s.serviceRequestId as serviceRequestId, COUNT(s) as count FROM ServiceRequestRemovalSuggestion s WHERE s.serviceRequestId IN (:serviceRequestIds) AND s.deleted = false GROUP BY s.serviceRequestId") + List countByServiceRequestIdIn(List serviceRequestIds); +} \ No newline at end of file diff --git a/app/src/main/java/app/model/servicerequest/ServiceRequestRepository.java b/app/src/main/java/app/model/servicerequest/ServiceRequestRepository.java index 614a6e56..6dbfa968 100644 --- a/app/src/main/java/app/model/servicerequest/ServiceRequestRepository.java +++ b/app/src/main/java/app/model/servicerequest/ServiceRequestRepository.java @@ -16,7 +16,9 @@ import app.model.jurisdiction.Jurisdiction_; import app.model.service.Service_; +import io.micronaut.data.annotation.Query; import io.micronaut.data.annotation.Repository; +import io.micronaut.data.annotation.Where; import io.micronaut.data.model.Page; import io.micronaut.data.model.Pageable; import io.micronaut.data.model.Sort; @@ -25,6 +27,7 @@ import io.micronaut.data.repository.jpa.JpaSpecificationExecutor; import io.micronaut.data.repository.jpa.criteria.QuerySpecification; import jakarta.transaction.Transactional; + import java.time.Instant; import java.util.List; import java.util.Optional; @@ -33,27 +36,15 @@ public interface ServiceRequestRepository extends PageableRepository, JpaSpecificationExecutor { - default Page findByIdInAndJurisdictionId(List serviceRequestIds, String jurisdictionId, - Pageable pageable) { - return findByIdInAndJurisdictionIdAndDeleted(serviceRequestIds, jurisdictionId, false, pageable); - } - - - default List findByIdInAndJurisdictionId(List serviceRequestIds, String jurisdictionId, Sort sort) { - return findByIdInAndJurisdictionIdAndDeleted(serviceRequestIds, jurisdictionId, false, sort); - } - - - default Optional findByIdAndJurisdictionId(Long serviceRequestId, String jurisdictionId) { - return findByIdAndJurisdictionIdAndDeleted(serviceRequestId, jurisdictionId, false); - } - - Page findByIdInAndJurisdictionIdAndDeleted(List serviceRequestIds, String jurisdictionId, boolean deleted, Pageable pageable); - List findByIdInAndJurisdictionIdAndDeleted(List serviceRequestIds, String jurisdictionId, boolean deleted, Sort sort); - Optional findByIdAndJurisdictionIdAndDeleted(Long serviceRequestId, String jurisdictionId, boolean deleted); + Page findByIdInAndJurisdictionId(List serviceRequestIds, String jurisdictionId, Pageable pageable); + List findByIdInAndJurisdictionId(List serviceRequestIds, String jurisdictionId, Sort sort); + Optional findByIdAndJurisdictionId(Long serviceRequestId, String jurisdictionId); - Integer updateDeletedByIdAndJurisdictionIdAndDeletedFalse(Long id, String jurisdictionId, boolean deleted); + @Query("update ServiceRequest sr set sr.deleted = true where sr.id = :id and sr.jurisdiction.id = :jurisdictionId and sr.deleted = false") + Integer delete(Long id, String jurisdictionId); + @Where("@.deleted = :deleted") + Optional findByIdAndDeleted(Long id, boolean deleted); @Transactional default Page findAllBy(String jurisdictionId, List serviceCodes, List status, List priority, diff --git a/app/src/main/java/app/service/servicerequest/ServiceRequestService.java b/app/src/main/java/app/service/servicerequest/ServiceRequestService.java index 389d4f37..2d4f946e 100644 --- a/app/src/main/java/app/service/servicerequest/ServiceRequestService.java +++ b/app/src/main/java/app/service/servicerequest/ServiceRequestService.java @@ -28,6 +28,9 @@ import app.model.servicedefinition.ServiceDefinitionAttributeRepository; import app.model.servicerequest.ServiceRequest; import app.model.servicerequest.ServiceRequestPriority; +import app.model.servicerequest.ServiceRequestRemovalSuggestion; +import app.model.servicerequest.ServiceRequestRemovalSuggestionCount; +import app.model.servicerequest.ServiceRequestRemovalSuggestionRepository; import app.model.servicerequest.ServiceRequestRepository; import app.model.servicerequest.ServiceRequestStatus; import app.recaptcha.ReCaptchaService; @@ -85,6 +88,7 @@ public InvalidServiceRequestException(String message) { private static final Logger LOG = LoggerFactory.getLogger(ServiceRequestService.class); private final ServiceRequestRepository serviceRequestRepository; + private final ServiceRequestRemovalSuggestionRepository removalSuggestionRepository; private final ServiceRepository serviceRepository; private final ServiceDefinitionAttributeRepository attributeRepository; private final ReCaptchaService reCaptchaService; @@ -94,6 +98,7 @@ public InvalidServiceRequestException(String message) { LibreGeometryFactory libreGeometryFactory; public ServiceRequestService(ServiceRequestRepository serviceRequestRepository, + ServiceRequestRemovalSuggestionRepository removalSuggestionRepository, ServiceRepository serviceRepository, ServiceDefinitionAttributeRepository attributeRepository, ReCaptchaService reCaptchaService, StorageUrlUtil storageUrlUtil, @@ -101,6 +106,7 @@ public ServiceRequestService(ServiceRequestRepository serviceRequestRepository, JurisdictionBoundaryService jurisdictionBoundaryService, LibreGeometryFactory libreGeometryFactory) { this.serviceRequestRepository = serviceRequestRepository; + this.removalSuggestionRepository = removalSuggestionRepository; this.serviceRepository = serviceRepository; this.attributeRepository = attributeRepository; this.reCaptchaService = reCaptchaService; @@ -173,6 +179,41 @@ public PostResponseServiceRequestDTO createServiceRequest(HttpRequest request return new PostResponseServiceRequestDTO(serviceRequestRepository.save(serviceRequest)); } + public void createRemovalSuggestion(Long serviceRequestId, String jurisdictionId, PostRequestServiceRequestRemovalSuggestionDTO suggestionDTO) { + reCaptchaService.verifyReCaptcha(suggestionDTO.getgRecaptchaResponse()); + Optional serviceRequestOptional = serviceRequestRepository.findByIdAndJurisdictionId(serviceRequestId, jurisdictionId); + if (serviceRequestOptional.isEmpty()) { + throw new InvalidServiceRequestException("Service Request not found."); + } + + ServiceRequestRemovalSuggestion suggestion = new ServiceRequestRemovalSuggestion(); + suggestion.setServiceRequestId(serviceRequestOptional.get().getId()); + suggestion.setJurisdictionId(jurisdictionId); + suggestion.setEmail(suggestionDTO.getEmail()); + suggestion.setName(suggestionDTO.getName()); + suggestion.setPhone(suggestionDTO.getPhone()); + suggestion.setReason(suggestionDTO.getReason()); + + removalSuggestionRepository.save(suggestion); + } + + public Page getRemovalSuggestions(String jurisdictionId, Pageable pageable) { + return removalSuggestionRepository.findAllByJurisdictionId(jurisdictionId, pageable) + .map(this::convertToSuggestionDTO); + } + + private ServiceRequestRemovalSuggestionDTO convertToSuggestionDTO(ServiceRequestRemovalSuggestion suggestion) { + ServiceRequestRemovalSuggestionDTO dto = new ServiceRequestRemovalSuggestionDTO(); + dto.setId(suggestion.getId()); + dto.setServiceRequestId(suggestion.getServiceRequestId()); + dto.setEmail(suggestion.getEmail()); + dto.setName(suggestion.getName()); + dto.setPhone(suggestion.getPhone()); + dto.setReason(suggestion.getReason()); + dto.setDateCreated(suggestion.getDateCreated()); + return dto; + } + private boolean validMediaUrl(String mediaUrl) { if (mediaUrl == null) return true; return mediaUrl.startsWith(storageUrlUtil.getBucketUrlString()); @@ -403,7 +444,20 @@ public Page findAll(GetServiceRequestsDTO requestDTO, String : ServiceRequestService::convertToDTO; - return getServiceRequestPage(requestDTO, jurisdictionId).map(mapper); + Page page = getServiceRequestPage(requestDTO, jurisdictionId); + Page dtoPage = page.map(mapper); + + if (canViewSensitive && !dtoPage.getContent().isEmpty()) { + List ids = dtoPage.getContent().stream().map(ServiceRequestDTO::getId).collect(Collectors.toList()); + List counts = removalSuggestionRepository.countByServiceRequestIdIn(ids); + Map countMap = counts.stream().collect(Collectors.toMap(ServiceRequestRemovalSuggestionCount::getServiceRequestId, ServiceRequestRemovalSuggestionCount::getCount)); + + dtoPage.getContent().forEach(dto -> { + dto.setRemovalSuggestionsCount(countMap.getOrDefault(dto.getId(), 0L)); + }); + } + + return dtoPage; } private Page getServiceRequestPage(GetServiceRequestsDTO requestDTO, String jurisdictionId) { @@ -545,6 +599,19 @@ private List getServiceRequests(GetServiceRequestsDTO requestDTO } public int delete(Long serviceRequestId, String jurisdictionId) { - return serviceRequestRepository.updateDeletedByIdAndJurisdictionIdAndDeletedFalse(serviceRequestId, jurisdictionId, true); + removalSuggestionRepository.deleteByServiceRequestIdAndJurisdictionId(serviceRequestId, jurisdictionId); + return serviceRequestRepository.delete(serviceRequestId, jurisdictionId); + } + + public void deleteRemovalSuggestion(Long id, String jurisdictionId) { + Optional suggestionOptional = removalSuggestionRepository.findById(id); + if (suggestionOptional.isPresent()) { + ServiceRequestRemovalSuggestion suggestion = suggestionOptional.get(); + if (suggestion.getJurisdictionId().equals(jurisdictionId)) { + removalSuggestionRepository.softDelete(id, jurisdictionId); + return; + } + } + throw new InvalidServiceRequestException("Removal suggestion not found."); } } diff --git a/app/src/main/resources/db/migration/V11__add_service_request_removal_suggestions.sql b/app/src/main/resources/db/migration/V11__add_service_request_removal_suggestions.sql new file mode 100644 index 00000000..9caae225 --- /dev/null +++ b/app/src/main/resources/db/migration/V11__add_service_request_removal_suggestions.sql @@ -0,0 +1,15 @@ +CREATE TABLE service_request_removal_suggestions ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + service_request_id BIGINT NOT NULL, + jurisdiction_id VARCHAR(255) NOT NULL, + email VARCHAR(255) NOT NULL, + name VARCHAR(255), + phone VARCHAR(255), + reason TEXT NOT NULL, + date_created TIMESTAMP NOT NULL, + deleted BOOL NOT NULL, + CONSTRAINT fk_service_request_removal_suggestions_service_request + FOREIGN KEY (service_request_id) + REFERENCES service_requests (id) + ON DELETE CASCADE +); diff --git a/app/src/test/java/app/service/servicerequest/ServiceRequestServiceTest.java b/app/src/test/java/app/service/servicerequest/ServiceRequestServiceTest.java index d7f85b14..160d58ba 100644 --- a/app/src/test/java/app/service/servicerequest/ServiceRequestServiceTest.java +++ b/app/src/test/java/app/service/servicerequest/ServiceRequestServiceTest.java @@ -120,7 +120,7 @@ void delete_shouldMarkServiceRequestAsDeletedAndReturnOneOnSuccess() throws Pars // Then assertEquals(1, updatedCount); - Optional deletedServiceRequestOptional = serviceRequestRepository.findById(serviceRequest.getId()); + Optional deletedServiceRequestOptional = serviceRequestRepository.findByIdAndDeleted(serviceRequest.getId(), true); assertTrue(deletedServiceRequestOptional.isPresent()); assertTrue(deletedServiceRequestOptional.get().isDeleted()); @@ -138,7 +138,7 @@ void delete_shouldReturnZeroWhenServiceRequestIsAlreadyDeleted() throws ParseExc // First delete int firstUpdateCount = serviceRequestService.delete(serviceRequest.getId(), testJurisdiction.getId()); assertEquals(1, firstUpdateCount); - Optional deletedServiceRequestOptional = serviceRequestRepository.findById(serviceRequest.getId()); + Optional deletedServiceRequestOptional = serviceRequestRepository.findByIdAndDeleted(serviceRequest.getId(), true); assertTrue(deletedServiceRequestOptional.isPresent()); assertTrue(deletedServiceRequestOptional.get().isDeleted()); diff --git a/frontend/src/lib/components/ConfirmationModal.svelte b/frontend/src/lib/components/ConfirmationModal.svelte new file mode 100644 index 00000000..2d98cbd0 --- /dev/null +++ b/frontend/src/lib/components/ConfirmationModal.svelte @@ -0,0 +1,29 @@ + + +{#if open} + + + + {title} + +

{message}

+
+ +
+ + +
+
+
+
+
+{/if} diff --git a/frontend/src/lib/components/RemovalSuggestionsList.svelte b/frontend/src/lib/components/RemovalSuggestionsList.svelte new file mode 100644 index 00000000..e69429c4 --- /dev/null +++ b/frontend/src/lib/components/RemovalSuggestionsList.svelte @@ -0,0 +1,106 @@ + + +{#if suggestions.length > 0} + +
+

Removal Suggestions

+ + {#each suggestions as suggestion (suggestion.id)} + +
+
+ {suggestion.email} + {toTimeStamp(suggestion.date_created)} +
+ {#if suggestion.name} + {suggestion.name} + {/if} + {#if suggestion.phone} + {suggestion.phone} + {/if} +

{suggestion.reason}

+
+ +
+
+
+ {/each} +
+
+
+ + (showConfirmationModal = false)} + handleConfirm={confirmDismiss} + loading={isDeleting} + /> +{/if} diff --git a/frontend/src/lib/components/ServiceRequest.svelte b/frontend/src/lib/components/ServiceRequest.svelte index a1e2ea2b..a7d39149 100644 --- a/frontend/src/lib/components/ServiceRequest.svelte +++ b/frontend/src/lib/components/ServiceRequest.svelte @@ -13,6 +13,8 @@ import ServiceRequestButtonsContainer from './ServiceRequestButtonsContainer.svelte'; import ServiceRequestStatusBadge from './ServiceRequestStatusBadge.svelte'; import AuthGuard from './AuthGuard.svelte'; + import ConfirmationModal from './ConfirmationModal.svelte'; + import RemovalSuggestionsList from './RemovalSuggestionsList.svelte'; const libre311 = useLibre311Service(); const alertError = useLibre311Context().alertError; @@ -23,6 +25,8 @@ export let back: string; let isUpdateButtonClicked: boolean = false; + let showDeleteModal = false; + let isDeleting = false; $: if ($page.url) { isUpdateButtonClicked = false; @@ -35,9 +39,12 @@ return `${serviceRequest.first_name ?? ''} ${serviceRequest.last_name ?? ''}`; } - async function deleteServiceReq() { - let confirmed = window.confirm('Are you sure you would like to delete this request?'); - if (!confirmed) return; + function deleteServiceReq() { + showDeleteModal = true; + } + + async function confirmDelete() { + isDeleting = true; try { await libre311.deleteServiceRequest({ service_request_id: serviceRequest.service_request_id @@ -51,6 +58,9 @@ goto('/issues/table'); } catch (error) { alertError(error); + } finally { + isDeleting = false; + showDeleteModal = false; } } @@ -77,8 +87,8 @@ } -
- +
+
@@ -234,8 +244,18 @@ {/if}
+
+ (showDeleteModal = false)} + handleConfirm={confirmDelete} + loading={isDeleting} +/> +