diff --git a/source/backend/api/Areas/Documents/DocumentQueueController.cs b/source/backend/api/Areas/Documents/DocumentQueueController.cs index 3a3fd79efa..4272c788cb 100644 --- a/source/backend/api/Areas/Documents/DocumentQueueController.cs +++ b/source/backend/api/Areas/Documents/DocumentQueueController.cs @@ -62,7 +62,7 @@ public DocumentQueueController(IDocumentQueueService documentQueueService, IMapp [ProducesResponseType(typeof(List), 200)] [SwaggerOperation(Tags = new[] { "document-types" })] [TypeFilter(typeof(NullJsonResultFilter))] - public IActionResult Update(long documentQueueId, [FromBody] DocumentQueueModel documentQueue) + public async Task Update(long documentQueueId, [FromBody] DocumentQueueModel documentQueue) { _logger.LogInformation( "Request received by Controller: {Controller}, Action: {ControllerAction}, User: {User}, DateTime: {DateTime}", @@ -77,7 +77,7 @@ public IActionResult Update(long documentQueueId, [FromBody] DocumentQueueModel throw new BadRequestException("Invalid document queue id."); } - var queuedDocuments = _documentQueueService.Update(_mapper.Map(documentQueue)); + var queuedDocuments = await _documentQueueService.Update(_mapper.Map(documentQueue)); var updatedDocumentQueue = _mapper.Map(queuedDocuments); return new JsonResult(updatedDocumentQueue); } diff --git a/source/backend/api/Areas/Tools/Controllers/GeocoderController.cs b/source/backend/api/Areas/Tools/Controllers/GeocoderController.cs index 49b3ff307a..fe9e4a9796 100644 --- a/source/backend/api/Areas/Tools/Controllers/GeocoderController.cs +++ b/source/backend/api/Areas/Tools/Controllers/GeocoderController.cs @@ -63,7 +63,7 @@ public GeocoderController(IGeocoderService geocoderService, IMapper mapper) [HasPermission(Permissions.PropertyEdit)] public async Task FindAddressesAsync(string address) { - var parameters = this.Request.QueryString.ParseQueryString(); + var parameters = Request.QueryString.ParseQueryString(); parameters.AddressString = address; var result = await _geocoderService.GetSiteAddressesAsync(parameters); return new JsonResult(_mapper.Map(result.Features)); @@ -99,7 +99,7 @@ public async Task FindPidsAsync(Guid siteId) [HasPermission(Permissions.PropertyEdit)] public async Task FindNearestAddressAsync(string point) { - var parameters = this.Request.QueryString.ParseQueryString(); + var parameters = Request.QueryString.ParseQueryString(); parameters.Point = point; var result = await _geocoderService.GetNearestSiteAsync(parameters); return new JsonResult(_mapper.Map(result)); @@ -118,7 +118,7 @@ public async Task FindNearestAddressAsync(string point) [HasPermission(Permissions.PropertyEdit)] public async Task FindNearAddressesAsync(string point) { - var parameters = this.Request.QueryString.ParseQueryString(); + var parameters = Request.QueryString.ParseQueryString(); parameters.Point = point; var result = await _geocoderService.GetNearSitesAsync(parameters); return new JsonResult(_mapper.Map(result.Features)); diff --git a/source/backend/api/Areas/Tools/Controllers/LtsaController.cs b/source/backend/api/Areas/Tools/Controllers/LtsaController.cs index ecf5818aa6..deaad97588 100644 --- a/source/backend/api/Areas/Tools/Controllers/LtsaController.cs +++ b/source/backend/api/Areas/Tools/Controllers/LtsaController.cs @@ -134,17 +134,17 @@ public async Task PostParcelInfoOrderAsync(string pid) } /// - /// Post a new order using default parameters and the passed in titleNumber. + /// Post a new order using default parameters and the passed in strataPlanNumber. /// /// the title number to create the order for. /// The order created within LTSA. [HttpPost("order/spcp")] [Produces("application/json")] [ProducesResponseType(typeof(Model.OrderWrapper), 200)] - [ProducesResponseType(typeof(Pims.Api.Models.ErrorResponseModel), 400)] + [ProducesResponseType(typeof(Models.ErrorResponseModel), 400)] [SwaggerOperation(Tags = new[] { "tools-ltsa" })] [HasPermission(Permissions.PropertyView)] - public async Task PostSpcpOrderAsync(string strataPlanNumber) + public async Task PostSpcpOrderAsync([FromQuery]string strataPlanNumber) { _logger.LogInformation( "Request received by Controller: {Controller}, Action: {ControllerAction}, User: {User}, DateTime: {DateTime}", @@ -154,6 +154,7 @@ public async Task PostSpcpOrderAsync(string strataPlanNumber) DateTime.Now); var result = await _ltsaService.PostSpcpOrder(strataPlanNumber); + return new JsonResult(result?.Order); } diff --git a/source/backend/api/Pims.Api.csproj b/source/backend/api/Pims.Api.csproj index d55a563577..afa0bc506d 100644 --- a/source/backend/api/Pims.Api.csproj +++ b/source/backend/api/Pims.Api.csproj @@ -2,11 +2,13 @@ 0ef6255f-9ea0-49ec-8c65-c172304b4926 - 5.11.0-106.2 + 5.11.0-106.25 5.11.0.106 true {16BC0468-78F6-4C91-87DA-7403C919E646} net8.0 + true + true @@ -17,6 +19,7 @@ + diff --git a/source/backend/api/Repositories/Mayan/MayanDocumentRepository.cs b/source/backend/api/Repositories/Mayan/MayanDocumentRepository.cs index bb3ff93ef7..4998175b90 100644 --- a/source/backend/api/Repositories/Mayan/MayanDocumentRepository.cs +++ b/source/backend/api/Repositories/Mayan/MayanDocumentRepository.cs @@ -257,8 +257,9 @@ public async Task> TryUploadDocumentAsync( string authenticationToken = await _authRepository.GetTokenAsync(); byte[] fileData; - using var byteReader = new BinaryReader(file.OpenReadStream()); - fileData = byteReader.ReadBytes((int)file.OpenReadStream().Length); + using Stream stream = file.OpenReadStream(); + using var byteReader = new BinaryReader(stream); + fileData = byteReader.ReadBytes((int)stream.Length); // Add the file data to the content using ByteArrayContent fileBytes = new(fileData); diff --git a/source/backend/api/Services/DocumentQueueService.cs b/source/backend/api/Services/DocumentQueueService.cs index 9c6c744bb1..7c38f4b270 100644 --- a/source/backend/api/Services/DocumentQueueService.cs +++ b/source/backend/api/Services/DocumentQueueService.cs @@ -10,9 +10,7 @@ using Microsoft.Extensions.Options; using Pims.Api.Models.CodeTypes; using Pims.Api.Models.Concepts.Document; -using Pims.Api.Models.Mayan.Document; using Pims.Api.Models.Requests.Document.Upload; -using Pims.Api.Models.Requests.Http; using Pims.Core.Api.Exceptions; using Pims.Core.Api.Services; using Pims.Core.Extensions; @@ -69,7 +67,6 @@ public PimsDocumentQueue GetById(long documentQueueId) { throw new KeyNotFoundException($"Unable to find queued document by id: ${documentQueueId}"); } - return documentQueue; } @@ -86,26 +83,9 @@ public IEnumerable SearchDocumentQueue(DocumentQueueFilter fi var queuedDocuments = _documentQueueRepository.GetAllByFilter(filter); - if (filter.MaxFileSize != null) - { - List documentsBelowMaxFileSize = new List(); - long totalFileSize = 0; - queuedDocuments.ForEach(currentDocument => - { - if (currentDocument.DocumentSize + totalFileSize <= filter.MaxFileSize) - { - totalFileSize += currentDocument.DocumentSize; - documentsBelowMaxFileSize.Add(currentDocument); - } - }); - if (documentsBelowMaxFileSize.Count == 0 && queuedDocuments.Any()) - { - documentsBelowMaxFileSize.Add(queuedDocuments.FirstOrDefault()); - } - this.Logger.LogDebug("returning {length} documents below file size", documentsBelowMaxFileSize.Count); - return documentsBelowMaxFileSize; - } - return queuedDocuments; + return filter.MaxFileSize != null + ? FilterDocumentsByMaxFileSize(queuedDocuments, filter.MaxFileSize.Value) + : queuedDocuments; } /// @@ -114,15 +94,15 @@ public IEnumerable SearchDocumentQueue(DocumentQueueFilter fi /// The document queue object to update. /// The updated document queue object. /// Thrown when the user is not authorized to perform this operation. - public PimsDocumentQueue Update(PimsDocumentQueue documentQueue) + public async Task Update(PimsDocumentQueue documentQueue) { this.Logger.LogInformation("Updating queued document {documentQueueId}", documentQueue.DocumentQueueId); this.Logger.LogDebug("Incoming queued document {document}", documentQueue.Serialize()); this.User.ThrowIfNotAuthorizedOrServiceAccount(Permissions.SystemAdmin, this._keycloakOptions); - _documentQueueRepository.Update(documentQueue); - _documentQueueRepository.CommitTransaction(); + await _documentQueueRepository.Update(documentQueue); + return documentQueue; } @@ -139,131 +119,183 @@ public async Task PollForDocument(PimsDocumentQueue documentQ this.Logger.LogDebug("Polling queued document {document}", documentQueue.Serialize()); this.User.ThrowIfNotAuthorizedOrServiceAccount(Permissions.SystemAdmin, this._keycloakOptions); - if (documentQueue.DocumentId == null) + + ValidateDocumentQueueForPolling(documentQueue); + + var databaseDocumentQueue = _documentQueueRepository.TryGetById(documentQueue.DocumentQueueId); + ValidateDatabaseDocumentQueueForPolling(databaseDocumentQueue, documentQueue.DocumentQueueId); + + var relatedDocument = _documentRepository.TryGet(documentQueue.DocumentId.Value); + await ValidateRelatedDocumentForPolling(relatedDocument, databaseDocumentQueue); + + return await PollDocumentStatusAsync(databaseDocumentQueue, relatedDocument); + } + + public async Task Upload(PimsDocumentQueue documentQueue) + { + this.Logger.LogInformation("Uploading queued document {documentQueueId}", documentQueue.DocumentQueueId); + this.Logger.LogDebug("Uploading queued document {document}", documentQueue.Serialize()); + + this.User.ThrowIfNotAuthorizedOrServiceAccount(Permissions.SystemAdmin, this._keycloakOptions); + + var databaseDocumentQueue = _documentQueueRepository.TryGetById(documentQueue.DocumentQueueId); + ValidateDatabaseDocumentQueueForUpload(databaseDocumentQueue, documentQueue.DocumentQueueId); + + databaseDocumentQueue.DocProcessStartDt = DateTime.UtcNow; + + if (!ValidateQueuedDocument(databaseDocumentQueue, documentQueue)) { - this.Logger.LogError("polled queued document does not have a document Id {documentQueueId}", documentQueue.DocumentQueueId); - throw new InvalidDataException("DocumentId is required to poll for a document."); + databaseDocumentQueue.MayanError = "Document is invalid."; + await UpdateDocumentQueueStatus(databaseDocumentQueue, DocumentQueueStatusTypes.PIMS_ERROR); + return databaseDocumentQueue; } - var databaseDocumentQueue = _documentQueueRepository.TryGetById(documentQueue.DocumentQueueId); - if (databaseDocumentQueue == null) + databaseDocumentQueue = HandleRetryForErroredDocument(databaseDocumentQueue); + + var relatedDocument = _documentRepository.TryGetDocumentRelationships(databaseDocumentQueue.DocumentId.Value); + await ValidateRelatedDocumentForUpload(relatedDocument, databaseDocumentQueue); + + return await UploadDocumentAsync(databaseDocumentQueue, relatedDocument); + } + + private List FilterDocumentsByMaxFileSize(IEnumerable queuedDocuments, long maxFileSize) + { + List documentsBelowMaxFileSize = new(); + long totalFileSize = 0; + + foreach (var currentDocument in queuedDocuments) { - this.Logger.LogError("Unable to find document queue with {id}", documentQueue.DocumentQueueId); - throw new KeyNotFoundException($"Unable to find document queue with matching id: {documentQueue.DocumentQueueId}"); + if (currentDocument.DocumentSize + totalFileSize <= maxFileSize) + { + totalFileSize += currentDocument.DocumentSize; + documentsBelowMaxFileSize.Add(currentDocument); + } } - else if (databaseDocumentQueue.DocumentQueueStatusTypeCode != DocumentQueueStatusTypes.PROCESSING.ToString()) + + if (documentsBelowMaxFileSize.Count == 0 && queuedDocuments.Any()) { - this.Logger.LogError("Document Queue {documentQueueId} is not in valid state, aborting poll.", documentQueue.DocumentQueueId); - return databaseDocumentQueue; + documentsBelowMaxFileSize.Add(queuedDocuments.First()); } - else if (databaseDocumentQueue.DocumentQueueStatusTypeCode == DocumentQueueStatusTypes.PENDING.ToString() || databaseDocumentQueue.DocumentQueueStatusTypeCode == DocumentQueueStatusTypes.SUCCESS.ToString()) + + this.Logger.LogDebug("Returning {length} documents below file size", documentsBelowMaxFileSize.Count); + return documentsBelowMaxFileSize; + } + + private void ValidateDocumentQueueForPolling(PimsDocumentQueue documentQueue) + { + if (documentQueue.DocumentId == null) { - this.Logger.LogError("Document Queue {documentQueueId} is not in valid state, aborting poll.", documentQueue.DocumentQueueId); - return databaseDocumentQueue; + this.Logger.LogError("Polled queued document does not have a document Id {documentQueueId}", documentQueue.DocumentQueueId); + throw new InvalidDataException("DocumentId is required to poll for a document."); } + } - var relatedDocument = _documentRepository.TryGet(documentQueue.DocumentId.Value); + private void ValidateDatabaseDocumentQueueForPolling(PimsDocumentQueue databaseDocumentQueue, long documentQueueId) + { + if (databaseDocumentQueue == null) + { + this.Logger.LogError("Unable to find document queue with {id}", documentQueueId); + throw new KeyNotFoundException($"Unable to find document queue with matching id: {documentQueueId}"); + } - if (relatedDocument?.MayanId == null || relatedDocument?.MayanId < 0) + if (databaseDocumentQueue.DocumentQueueStatusTypeCode != DocumentQueueStatusTypes.PROCESSING.ToString()) { - this.Logger.LogError("Queued Document {documentQueueId} has no mayan id and is invalid.", documentQueue.DocumentQueueId); + this.Logger.LogError("Document Queue {documentQueueId} is not in valid state, aborting poll.", documentQueueId); + throw new InvalidOperationException("Document queue is not in a valid state for polling."); + } + } + + private async Task ValidateRelatedDocumentForPolling(PimsDocument relatedDocument, PimsDocumentQueue databaseDocumentQueue) + { + if (relatedDocument?.MayanId == null || relatedDocument.MayanId < 0) + { + this.Logger.LogError("Queued Document {documentQueueId} has no Mayan ID and is invalid.", databaseDocumentQueue.DocumentQueueId); databaseDocumentQueue.MayanError = "Document does not have a valid MayanId."; - UpdateDocumentQueueStatus(databaseDocumentQueue, DocumentQueueStatusTypes.PIMS_ERROR); - return databaseDocumentQueue; + await UpdateDocumentQueueStatus(databaseDocumentQueue, DocumentQueueStatusTypes.PIMS_ERROR); + throw new InvalidDataException("Document does not have a valid MayanId."); } + } - ExternalResponse documentDetailsResponse = await _documentService.GetStorageDocumentDetail(relatedDocument.MayanId.Value); + private async Task PollDocumentStatusAsync(PimsDocumentQueue databaseDocumentQueue, PimsDocument relatedDocument) + { + var documentDetailsResponse = await _documentService.GetStorageDocumentDetail(relatedDocument.MayanId.Value); - if (documentDetailsResponse.Status != ExternalResponseStatus.Success || documentDetailsResponse?.Payload == null) + if (documentDetailsResponse.Status != ExternalResponseStatus.Success || documentDetailsResponse.Payload == null) { - this.Logger.LogError("Polling for queued document {documentQueueId} failed with status {documentDetailsResponseStatus}", documentQueue.DocumentQueueId, documentDetailsResponse.Status); + this.Logger.LogError("Polling for queued document {documentQueueId} failed with status {documentDetailsResponseStatus}", databaseDocumentQueue.DocumentQueueId, documentDetailsResponse.Status); databaseDocumentQueue.MayanError = "Document Polling failed."; - UpdateDocumentQueueStatus(databaseDocumentQueue, DocumentQueueStatusTypes.PIMS_ERROR); + await UpdateDocumentQueueStatus(databaseDocumentQueue, DocumentQueueStatusTypes.PIMS_ERROR); return databaseDocumentQueue; } if (documentDetailsResponse.Payload.FileLatest?.Id == null) { - this.Logger.LogInformation("Polling for queued document {documentQueueId} complete, file still processing", documentQueue.DocumentQueueId); + this.Logger.LogInformation("Polling for queued document {documentQueueId} complete, file still processing", databaseDocumentQueue.DocumentQueueId); } else { - this.Logger.LogInformation("Polling for queued document {documentQueueId} complete, file uploaded successfully", documentQueue.DocumentQueueId); - UpdateDocumentQueueStatus(databaseDocumentQueue, DocumentQueueStatusTypes.SUCCESS); + this.Logger.LogInformation("Polling for queued document {documentQueueId} complete, file uploaded successfully", databaseDocumentQueue.DocumentQueueId); + await UpdateDocumentQueueStatus(databaseDocumentQueue, DocumentQueueStatusTypes.SUCCESS); } return databaseDocumentQueue; } - /// - /// Uploads the specified document queue. - /// - /// The document queue object containing the document to upload. - /// A task that represents the asynchronous operation. The task result contains the updated document queue object, or null if the upload failed. - /// Thrown when the user is not authorized to perform this operation. - /// Thrown when the document queue does not have a valid document ID or related document. - public async Task Upload(PimsDocumentQueue documentQueue) + private void ValidateDatabaseDocumentQueueForUpload(PimsDocumentQueue databaseDocumentQueue, long documentQueueId) { - this.Logger.LogInformation("Uploading queued document {documentQueueId}", documentQueue.DocumentQueueId); - this.Logger.LogDebug("Uploading queued document {document}", documentQueue.Serialize()); - - this.User.ThrowIfNotAuthorizedOrServiceAccount(Permissions.SystemAdmin, this._keycloakOptions); - - var databaseDocumentQueue = _documentQueueRepository.TryGetById(documentQueue.DocumentQueueId); if (databaseDocumentQueue == null) { - this.Logger.LogError("Unable to find document queue with {id}", documentQueue.DocumentQueueId); - throw new KeyNotFoundException($"Unable to find document queue with matching id: {documentQueue.DocumentQueueId}"); - } - databaseDocumentQueue.DocProcessStartDt = DateTime.UtcNow; - - bool isValid = ValidateQueuedDocument(databaseDocumentQueue, documentQueue); - if (!isValid) - { - this.Logger.LogDebug("Document Queue {documentQueueId}, invalid, aborting upload.", documentQueue.DocumentQueueId); - databaseDocumentQueue.MayanError = "Document is invalid."; - UpdateDocumentQueueStatus(databaseDocumentQueue, DocumentQueueStatusTypes.PIMS_ERROR); - return databaseDocumentQueue; + this.Logger.LogError("Unable to find document queue with {id}", documentQueueId); + throw new KeyNotFoundException($"Unable to find document queue with matching id: {documentQueueId}"); } + } - // if the document queued for upload is already in an error state, update the retries. - if (databaseDocumentQueue.DocumentQueueStatusTypeCode == DocumentQueueStatusTypes.PIMS_ERROR.ToString() || databaseDocumentQueue.DocumentQueueStatusTypeCode == DocumentQueueStatusTypes.MAYAN_ERROR.ToString()) + private PimsDocumentQueue HandleRetryForErroredDocument(PimsDocumentQueue databaseDocumentQueue) + { + if (databaseDocumentQueue.DocumentQueueStatusTypeCode == DocumentQueueStatusTypes.PIMS_ERROR.ToString() || + databaseDocumentQueue.DocumentQueueStatusTypeCode == DocumentQueueStatusTypes.MAYAN_ERROR.ToString()) { - this.Logger.LogDebug("Document Queue {documentQueueId}, previously errored, retrying", documentQueue.DocumentQueueId); + this.Logger.LogDebug("Document Queue {documentQueueId}, previously errored, retrying", databaseDocumentQueue.DocumentQueueId); databaseDocumentQueue.DocProcessRetries = ++databaseDocumentQueue.DocProcessRetries ?? 1; databaseDocumentQueue.DocProcessEndDt = null; } + return databaseDocumentQueue; + } - PimsDocument relatedDocument = null; - relatedDocument = _documentRepository.TryGetDocumentRelationships(databaseDocumentQueue.DocumentId.Value); + private async Task ValidateRelatedDocumentForUpload(PimsDocument relatedDocument, PimsDocumentQueue databaseDocumentQueue) + { if (relatedDocument?.DocumentTypeId == null) { databaseDocumentQueue.MayanError = "Document does not have a valid DocumentType."; this.Logger.LogError("Queued document {documentQueueId} does not have a related PIMS_DOCUMENT {documentId} with valid DocumentType, aborting.", databaseDocumentQueue.DocumentQueueId, relatedDocument?.DocumentId); - UpdateDocumentQueueStatus(databaseDocumentQueue, DocumentQueueStatusTypes.PIMS_ERROR); - return databaseDocumentQueue; + await UpdateDocumentQueueStatus(databaseDocumentQueue, DocumentQueueStatusTypes.PIMS_ERROR); + throw new InvalidDataException("Document does not have a valid DocumentType."); } + } + private async Task UploadDocumentAsync(PimsDocumentQueue databaseDocumentQueue, PimsDocument relatedDocument) + { try { - PimsDocumentTyp documentTyp = _documentTypeRepository.GetById(relatedDocument.DocumentTypeId); // throws KeyNotFoundException if not found. + var documentType = _documentTypeRepository.GetById(relatedDocument.DocumentTypeId); - IFormFile file = null; - using MemoryStream memStream = new(databaseDocumentQueue.Document); - file = new FormFile(memStream, 0, databaseDocumentQueue.Document.Length, relatedDocument.FileName, relatedDocument.FileName); + using var memStream = new MemoryStream(databaseDocumentQueue.Document); + var file = new FormFile(memStream, 0, databaseDocumentQueue.Document.Length, relatedDocument.FileName, relatedDocument.FileName); - DocumentUploadRequest request = new DocumentUploadRequest() + var request = new DocumentUploadRequest { File = file, DocumentStatusCode = relatedDocument.DocumentStatusTypeCode, DocumentTypeId = relatedDocument.DocumentTypeId, - DocumentTypeMayanId = documentTyp.MayanId, + DocumentTypeMayanId = documentType.MayanId, DocumentId = relatedDocument.DocumentId, - DocumentMetadata = databaseDocumentQueue.DocumentMetadata != null ? JsonSerializer.Deserialize>(databaseDocumentQueue.DocumentMetadata) : null, + DocumentMetadata = databaseDocumentQueue.DocumentMetadata != null + ? JsonSerializer.Deserialize>(databaseDocumentQueue.DocumentMetadata) + : null, }; - this.Logger.LogDebug("Document Queue {documentQueueId}, beginning upload.", documentQueue.DocumentQueueId); - DocumentUploadResponse response = await _documentService.UploadDocumentAsync(request, true); - UpdateDocumentQueueStatus(databaseDocumentQueue, DocumentQueueStatusTypes.PROCESSING); // Set the status to processing, as the document is now being uploaded. Must be set after the mayan id is set, so that the poll logic functions correctly. + + this.Logger.LogDebug("Document Queue {documentQueueId}, beginning upload.", databaseDocumentQueue.DocumentQueueId); + var response = await _documentService.UploadDocumentAsync(request, true); if (response.DocumentExternalResponse.Status != ExternalResponseStatus.Success || response?.DocumentExternalResponse?.Payload == null) { @@ -273,42 +305,40 @@ public async Task Upload(PimsDocumentQueue documentQueue) databaseDocumentQueue.DocumentQueueStatusTypeCode, response.DocumentExternalResponse.Status); - databaseDocumentQueue.MayanError = $"Failed to upload document, mayan error: {response.DocumentExternalResponse.Message}"; - UpdateDocumentQueueStatus(databaseDocumentQueue, DocumentQueueStatusTypes.MAYAN_ERROR); + databaseDocumentQueue.MayanError = $"Failed to upload document, Mayan error: {response.DocumentExternalResponse.Message}"; + await UpdateDocumentQueueStatus(databaseDocumentQueue, DocumentQueueStatusTypes.MAYAN_ERROR); return databaseDocumentQueue; } - response.MetadataExternalResponse.Where(r => r.Status != ExternalResponseStatus.Success).ForEach(r => this.Logger.LogError("url: ${url} status: ${status} message ${message}", r.Payload.Url, r.Status, r.Message)); // Log any metadata errors, but don't fail the upload. - // Mayan may have already returned a file id from the original upload. If not, this job will remain in the processing state (to be periodically checked for completion in another job). + response.MetadataExternalResponse + .Where(r => r.Status != ExternalResponseStatus.Success) + .ToList() + .ForEach(r => this.Logger.LogError("url: ${url} status: ${status} message ${message}", r.Payload.Url, r.Status, r.Message)); + if (response.DocumentExternalResponse?.Payload?.FileLatest?.Id != null) { - UpdateDocumentQueueStatus(databaseDocumentQueue, DocumentQueueStatusTypes.SUCCESS); + await UpdateDocumentQueueStatus(databaseDocumentQueue, DocumentQueueStatusTypes.SUCCESS); + } + else + { + await UpdateDocumentQueueStatus(databaseDocumentQueue, DocumentQueueStatusTypes.PROCESSING); } } catch (Exception ex) when (ex is BadRequestException || ex is KeyNotFoundException || ex is InvalidDataException || ex is JsonException) { this.Logger.LogError($"Error: {ex.Message}"); databaseDocumentQueue.MayanError = ex.Message; - UpdateDocumentQueueStatus(databaseDocumentQueue, DocumentQueueStatusTypes.PIMS_ERROR); + await UpdateDocumentQueueStatus(databaseDocumentQueue, DocumentQueueStatusTypes.PIMS_ERROR); } + return databaseDocumentQueue; } - /// - /// Updates the status of the specified document queue. - /// - /// The document queue object to update. - /// The new status type to set for the document queue. - /// - /// This method updates the document queue's status and commits the transaction. - /// If the status is a final state, it also updates the processing end date. - /// - private void UpdateDocumentQueueStatus(PimsDocumentQueue documentQueue, DocumentQueueStatusTypes statusType) + private async Task UpdateDocumentQueueStatus(PimsDocumentQueue documentQueue, DocumentQueueStatusTypes statusType) { documentQueue.DocumentQueueStatusTypeCode = statusType.ToString(); bool removeDocument = false; - // Any final states should update the processing end date. if (statusType != DocumentQueueStatusTypes.PROCESSING && statusType != DocumentQueueStatusTypes.PENDING) { documentQueue.DocProcessEndDt = DateTime.UtcNow; @@ -318,20 +348,10 @@ private void UpdateDocumentQueueStatus(PimsDocumentQueue documentQueue, Document removeDocument = true; } } - _documentQueueRepository.Update(documentQueue, removeDocument); - _documentQueueRepository.CommitTransaction(); + + await _documentQueueRepository.Update(documentQueue, removeDocument); } - /// - /// Validates the queued document against the database document queue. - /// - /// The document queue object from the database. - /// The document queue object to validate against the database. - /// True if the queued document is valid; otherwise, false. - /// - /// This method checks if the status type, process retries, and document content are valid. - /// It also ensures that at least one file document ID is associated with the document. - /// private bool ValidateQueuedDocument(PimsDocumentQueue databaseDocumentQueue, PimsDocumentQueue externalDocument) { if (databaseDocumentQueue.DocumentQueueStatusTypeCode != externalDocument.DocumentQueueStatusTypeCode) @@ -339,16 +359,19 @@ private bool ValidateQueuedDocument(PimsDocumentQueue databaseDocumentQueue, Pim this.Logger.LogError("Requested document queue status: {documentQueueStatusTypeCode} does not match current database status: {documentQueueStatusTypeCode}", externalDocument.DocumentQueueStatusTypeCode, databaseDocumentQueue.DocumentQueueStatusTypeCode); return false; } - else if (databaseDocumentQueue.DocProcessRetries != externalDocument.DocProcessRetries) + + if (databaseDocumentQueue.DocProcessRetries != externalDocument.DocProcessRetries) { this.Logger.LogError("Requested document retries: {documentQueueStatusTypeCode} does not match current database retries: {documentQueueStatusTypeCode}", externalDocument.DocumentQueueStatusTypeCode, databaseDocumentQueue.DocumentQueueStatusTypeCode); return false; } - else if (databaseDocumentQueue.Document == null || databaseDocumentQueue.DocumentId == null) + + if (databaseDocumentQueue.Document == null || databaseDocumentQueue.DocumentId == null) { this.Logger.LogError("Queued document file content is empty, unable to upload."); return false; } + return true; } } diff --git a/source/backend/api/Services/DocumentSyncService.cs b/source/backend/api/Services/DocumentSyncService.cs index be57b73a22..1e181c5c6f 100644 --- a/source/backend/api/Services/DocumentSyncService.cs +++ b/source/backend/api/Services/DocumentSyncService.cs @@ -234,7 +234,7 @@ public ExternalBatchResult SyncPimsToMayan(SyncModel model) IList>> updateTasks = new List>>(); foreach (var pimsDocumentTyp in pimsDocumentTypes) { - var matchingTypeFromMayan = mayanDocumentTypes.Payload.Results.FirstOrDefault(x => x.Id == pimsDocumentTyp.MayanId); + var matchingTypeFromMayan = mayanDocumentTypes?.Payload?.Results?.FirstOrDefault(x => x.Id == pimsDocumentTyp.MayanId); if (matchingTypeFromMayan == null) { createMayanDocumentTypeTasks.Add(new AddDocumentToMayanWithNameTaskWrapper() @@ -262,7 +262,7 @@ public ExternalBatchResult SyncPimsToMayan(SyncModel model) if (model.RemoveLingeringDocumentTypes) { // Delete the document types that are not in the PIMS db - foreach (var mayanDocumentTypeToRemove in mayanDocumentTypes.Payload.Results) + foreach (var mayanDocumentTypeToRemove in mayanDocumentTypes?.Payload?.Results) { if (pimsDocumentTypes.FirstOrDefault(x => x.MayanId == mayanDocumentTypeToRemove.Id) == null) { diff --git a/source/backend/api/Services/IDocumentQueueService.cs b/source/backend/api/Services/IDocumentQueueService.cs index 2a797d7df8..5a73fdb397 100644 --- a/source/backend/api/Services/IDocumentQueueService.cs +++ b/source/backend/api/Services/IDocumentQueueService.cs @@ -14,7 +14,7 @@ public interface IDocumentQueueService public IEnumerable SearchDocumentQueue(DocumentQueueFilter filter); - public PimsDocumentQueue Update(PimsDocumentQueue documentQueue); + public Task Update(PimsDocumentQueue documentQueue); public Task PollForDocument(PimsDocumentQueue documentQueue); diff --git a/source/backend/api/Services/ManagementFileService.cs b/source/backend/api/Services/ManagementFileService.cs index d771b7cfd7..aea2c50eae 100644 --- a/source/backend/api/Services/ManagementFileService.cs +++ b/source/backend/api/Services/ManagementFileService.cs @@ -12,6 +12,7 @@ using Pims.Dal.Entities; using Pims.Dal.Entities.Models; using Pims.Dal.Exceptions; +using Pims.Dal.Helpers.Extensions; using Pims.Dal.Repositories; namespace Pims.Api.Services @@ -26,7 +27,7 @@ public class ManagementFileService : IManagementFileService private readonly IPropertyService _propertyService; private readonly ILookupRepository _lookupRepository; private readonly INoteRelationshipRepository _entityNoteRepository; - private readonly IManagementStatusSolver _managementStatusSolver; + private readonly IManagementFileStatusSolver _managementStatusSolver; private readonly IPropertyOperationService _propertyOperationService; private readonly IPropertyActivityRepository _propertyActivityRepository; @@ -39,7 +40,7 @@ public ManagementFileService( IPropertyService propertyService, ILookupRepository lookupRepository, INoteRelationshipRepository entityNoteRepository, - IManagementStatusSolver managementStatusSolver, + IManagementFileStatusSolver managementStatusSolver, IPropertyOperationService propertyOperationService, IPropertyActivityRepository propertyActivityRepository) { @@ -71,6 +72,7 @@ public PimsManagementFile Add(PimsManagementFile managementFile, IEnumerable overrideCodes) { foreach (var managementProperty in managementFile.PimsManagementFileProperties) @@ -418,5 +452,7 @@ private void ValidateVersion(long managementFileId, long managementFileVersion) return currentManagementFileStatus; } + + } } diff --git a/source/backend/api/Solvers/AcquisitionStatusSolver.cs b/source/backend/api/Solvers/AcquisitionStatusSolver.cs index 184186ff02..7689e207e4 100644 --- a/source/backend/api/Solvers/AcquisitionStatusSolver.cs +++ b/source/backend/api/Solvers/AcquisitionStatusSolver.cs @@ -72,8 +72,8 @@ public bool CanEditTakes(AcquisitionStatusTypes? acquisitionStatus) var canEdit = acquisitionStatus switch { - AcquisitionStatusTypes.ACTIVE or AcquisitionStatusTypes.DRAFT or AcquisitionStatusTypes.HOLD => true, - AcquisitionStatusTypes.ARCHIV or AcquisitionStatusTypes.CANCEL or AcquisitionStatusTypes.CLOSED or AcquisitionStatusTypes.COMPLT => false, + AcquisitionStatusTypes.ACTIVE or AcquisitionStatusTypes.DRAFT => true, + AcquisitionStatusTypes.ARCHIV or AcquisitionStatusTypes.CANCEL or AcquisitionStatusTypes.CLOSED or AcquisitionStatusTypes.COMPLT or AcquisitionStatusTypes.HOLD => false, _ => false, }; return canEdit; diff --git a/source/backend/api/Solvers/IManagementFileStatusSolver.cs b/source/backend/api/Solvers/IManagementFileStatusSolver.cs new file mode 100644 index 0000000000..fde647efa1 --- /dev/null +++ b/source/backend/api/Solvers/IManagementFileStatusSolver.cs @@ -0,0 +1,21 @@ +using Pims.Api.Models.CodeTypes; + +namespace Pims.Api.Services +{ + public interface IManagementFileStatusSolver + { + bool IsAdminProtected(ManagementFileStatusTypes? managementStatus); + + bool CanEditDetails(ManagementFileStatusTypes? managementStatus); + + bool CanEditProperties(ManagementFileStatusTypes? managementStatus); + + bool CanEditDocuments(ManagementFileStatusTypes? managementStatus); + + bool CanEditNotes(ManagementFileStatusTypes? managementStatus); + + bool CanEditActivities(ManagementFileStatusTypes? managementStatus); + + ManagementFileStatusTypes? GetCurrentManagementStatus(string pimsManagementStatusType); + } +} diff --git a/source/backend/api/Solvers/IManagementStatusSolver.cs b/source/backend/api/Solvers/IManagementStatusSolver.cs deleted file mode 100644 index 4fb3e00007..0000000000 --- a/source/backend/api/Solvers/IManagementStatusSolver.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Pims.Api.Models.CodeTypes; - -namespace Pims.Api.Services -{ - public interface IManagementStatusSolver - { - bool CanEditDetails(ManagementFileStatusTypes? managementStatus); - - bool CanEditProperties(ManagementFileStatusTypes? managementStatus); - - ManagementFileStatusTypes? GetCurrentManagementStatus(string pimsManagementStatusType); - } -} diff --git a/source/backend/api/Solvers/ManagementFileStatusSolver.cs b/source/backend/api/Solvers/ManagementFileStatusSolver.cs new file mode 100644 index 0000000000..080883872f --- /dev/null +++ b/source/backend/api/Solvers/ManagementFileStatusSolver.cs @@ -0,0 +1,117 @@ +using System; +using Pims.Api.Models.CodeTypes; + +namespace Pims.Api.Services +{ + public class ManagementFileStatusSolver : IManagementFileStatusSolver + { + public bool IsAdminProtected(ManagementFileStatusTypes? managementStatus) + { + if (managementStatus == null) + { + return false; + } + + var canEdit = managementStatus switch + { + ManagementFileStatusTypes.ACTIVE or ManagementFileStatusTypes.DRAFT or ManagementFileStatusTypes.THIRDRDPARTY or ManagementFileStatusTypes.HOLD or ManagementFileStatusTypes.CANCELLED => false, + ManagementFileStatusTypes.ARCHIVED or ManagementFileStatusTypes.COMPLETE => true, + _ => false, + }; + return canEdit; + } + + public bool CanEditDetails(ManagementFileStatusTypes? managementStatus) + { + if (managementStatus == null) + { + return false; + } + + var canEdit = managementStatus switch + { + ManagementFileStatusTypes.ACTIVE or ManagementFileStatusTypes.DRAFT or ManagementFileStatusTypes.THIRDRDPARTY => true, + ManagementFileStatusTypes.HOLD or ManagementFileStatusTypes.ARCHIVED or ManagementFileStatusTypes.CANCELLED or ManagementFileStatusTypes.COMPLETE => false, + _ => false, + }; + return canEdit; + } + + public bool CanEditProperties(ManagementFileStatusTypes? managementStatus) + { + if (managementStatus == null) + { + return false; + } + + var canEdit = managementStatus switch + { + ManagementFileStatusTypes.ACTIVE or ManagementFileStatusTypes.DRAFT or ManagementFileStatusTypes.THIRDRDPARTY => true, + ManagementFileStatusTypes.ARCHIVED or ManagementFileStatusTypes.HOLD or ManagementFileStatusTypes.CANCELLED or ManagementFileStatusTypes.COMPLETE => false, + _ => false, + }; + return canEdit; + } + + public bool CanEditDocuments(ManagementFileStatusTypes? managementStatus) + { + if (managementStatus == null) + { + return false; + } + + var canEdit = managementStatus switch + { + ManagementFileStatusTypes.ACTIVE or ManagementFileStatusTypes.DRAFT or ManagementFileStatusTypes.HOLD or ManagementFileStatusTypes.CANCELLED or ManagementFileStatusTypes.COMPLETE or ManagementFileStatusTypes.THIRDRDPARTY => true, + ManagementFileStatusTypes.ARCHIVED => false, + _ => false, + }; + + return canEdit; + } + + public bool CanEditNotes(ManagementFileStatusTypes? managementStatus) + { + if (managementStatus == null) + { + return false; + } + + var canEditNotes = managementStatus switch + { + ManagementFileStatusTypes.ACTIVE or ManagementFileStatusTypes.DRAFT or ManagementFileStatusTypes.HOLD or ManagementFileStatusTypes.CANCELLED or ManagementFileStatusTypes.COMPLETE or ManagementFileStatusTypes.THIRDRDPARTY => true, + ManagementFileStatusTypes.ARCHIVED => false, + _ => false, + }; + + return canEditNotes; + } + + public bool CanEditActivities(ManagementFileStatusTypes? managementStatus) + { + if (managementStatus == null) + { + return false; + } + + var canEditActivities = managementStatus switch + { + ManagementFileStatusTypes.ACTIVE or ManagementFileStatusTypes.DRAFT or ManagementFileStatusTypes.THIRDRDPARTY => true, + ManagementFileStatusTypes.HOLD or ManagementFileStatusTypes.ARCHIVED or ManagementFileStatusTypes.CANCELLED or ManagementFileStatusTypes.COMPLETE => false, + _ => false, + }; + return canEditActivities; + } + + public ManagementFileStatusTypes? GetCurrentManagementStatus(string pimsManagementStatusType) + { + ManagementFileStatusTypes currentManagementStatus; + if (Enum.TryParse(pimsManagementStatusType, out currentManagementStatus)) + { + return currentManagementStatus; + } + + return currentManagementStatus; + } + } +} diff --git a/source/backend/api/Solvers/ManagementStatusSolver.cs b/source/backend/api/Solvers/ManagementStatusSolver.cs deleted file mode 100644 index 05791976e8..0000000000 --- a/source/backend/api/Solvers/ManagementStatusSolver.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System; -using Pims.Api.Models.CodeTypes; - -namespace Pims.Api.Services -{ - public class ManagementStatusSolver : IManagementStatusSolver - { - public bool CanEditDetails(ManagementFileStatusTypes? managementStatus) - { - if (managementStatus == null) - { - return false; - } - - bool canEdit; - switch (managementStatus) - { - case ManagementFileStatusTypes.ACTIVE: - case ManagementFileStatusTypes.DRAFT: - case ManagementFileStatusTypes.HOLD: - case ManagementFileStatusTypes.THIRDRDPARTY: - canEdit = true; - break; - case ManagementFileStatusTypes.ARCHIVED: - case ManagementFileStatusTypes.CANCELLED: - case ManagementFileStatusTypes.COMPLETE: - canEdit = false; - break; - default: - canEdit = false; - break; - } - - return canEdit; - } - - public bool CanEditProperties(ManagementFileStatusTypes? managementStatus) - { - if (managementStatus == null) - { - return false; - } - - bool canEdit; - switch (managementStatus) - { - case ManagementFileStatusTypes.ACTIVE: - case ManagementFileStatusTypes.DRAFT: - case ManagementFileStatusTypes.HOLD: - case ManagementFileStatusTypes.THIRDRDPARTY: - canEdit = true; - break; - case ManagementFileStatusTypes.ARCHIVED: - case ManagementFileStatusTypes.CANCELLED: - case ManagementFileStatusTypes.COMPLETE: - canEdit = false; - break; - default: - canEdit = false; - break; - } - - return canEdit; - } - - public ManagementFileStatusTypes? GetCurrentManagementStatus(string pimsManagementStatusType) - { - ManagementFileStatusTypes currentManagementStatus; - if (Enum.TryParse(pimsManagementStatusType, out currentManagementStatus)) - { - return currentManagementStatus; - } - - return currentManagementStatus; - } - } -} diff --git a/source/backend/api/Startup.cs b/source/backend/api/Startup.cs index 2128243d3a..624d26a3f0 100644 --- a/source/backend/api/Startup.cs +++ b/source/backend/api/Startup.cs @@ -16,6 +16,8 @@ using HealthChecks.SqlServer; using HealthChecks.UI.Client; using Mapster; +using Medallion.Threading; +using Medallion.Threading.SqlServer; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics.HealthChecks; @@ -227,6 +229,7 @@ public void ConfigureServices(IServiceCollection services) services.AddHttpClient("Pims.Api.Logging").AddHttpMessageHandler(); services.AddPimsContext(this.Environment, csBuilder.ConnectionString); services.AddPimsDalRepositories(); + services.AddSingleton(new SqlDistributedSynchronizationProvider(csBuilder.ConnectionString)); AddPimsApiRepositories(services); AddPimsApiServices(services); services.AddPimsKeycloakService(); @@ -566,7 +569,7 @@ private static void AddPimsApiServices(IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); + services.AddScoped(); } /// diff --git a/source/backend/apimodels/Models/Concepts/File/FilePropertyModel.cs b/source/backend/apimodels/Models/Concepts/File/FilePropertyModel.cs index 74f43ad609..2945c81b34 100644 --- a/source/backend/apimodels/Models/Concepts/File/FilePropertyModel.cs +++ b/source/backend/apimodels/Models/Concepts/File/FilePropertyModel.cs @@ -37,6 +37,11 @@ public class FilePropertyModel : BaseConcurrentModel /// public PropertyModel Property { get; set; } + /// + /// get/set - Optional flag indicating if the relationship is active. + /// + public bool? IsActive { get; set; } + /// /// get/set - The relationship's property id. /// diff --git a/source/backend/apimodels/Models/Concepts/ManagementFile/ManagementFilePropertyMap.cs b/source/backend/apimodels/Models/Concepts/ManagementFile/ManagementFilePropertyMap.cs index 5af6ce2cbb..311909529d 100644 --- a/source/backend/apimodels/Models/Concepts/ManagementFile/ManagementFilePropertyMap.cs +++ b/source/backend/apimodels/Models/Concepts/ManagementFile/ManagementFilePropertyMap.cs @@ -17,6 +17,7 @@ public void Register(TypeAdapterConfig config) .Map(dest => dest.DisplayOrder, src => src.DisplayOrder) .Map(dest => dest.PropertyId, src => src.PropertyId) .Map(dest => dest.Property, src => src.Property) + .Map(dest => dest.IsActive, src => src.IsActive) .Inherits(); // Map from Model to Entity @@ -28,6 +29,7 @@ public void Register(TypeAdapterConfig config) .Map(dest => dest.DisplayOrder, src => src.DisplayOrder) .Map(dest => dest.PropertyId, src => src.PropertyId) .Map(dest => dest.Property, src => src.Property) + .Map(dest => dest.IsActive, src => src.IsActive) .Inherits(); } } diff --git a/source/backend/core.api/Middleware/LogRequestMiddleware.cs b/source/backend/core.api/Middleware/LogRequestMiddleware.cs index e2aa599eb6..c420b76781 100644 --- a/source/backend/core.api/Middleware/LogRequestMiddleware.cs +++ b/source/backend/core.api/Middleware/LogRequestMiddleware.cs @@ -48,13 +48,13 @@ public async Task Invoke(HttpContext context) { context.Request.EnableBuffering(); await using var requestStream = _recyclableMemoryStreamManager.GetStream(); - await context.Request.Body.CopyToAsync(requestStream); - string body = null; - requestStream.Position = 0; - using (var streamReader = new StreamReader(requestStream)) + if (context.Request.ContentLength < maxStreamLength) { - if (requestStream.Length < maxStreamLength) + await context.Request.Body.CopyToAsync(requestStream); + + requestStream.Position = 0; + using (var streamReader = new StreamReader(requestStream)) { body = streamReader.ReadToEnd(); } diff --git a/source/backend/core.api/Middleware/LogResponseMiddleware.cs b/source/backend/core.api/Middleware/LogResponseMiddleware.cs index 1b9a952295..a180338b30 100644 --- a/source/backend/core.api/Middleware/LogResponseMiddleware.cs +++ b/source/backend/core.api/Middleware/LogResponseMiddleware.cs @@ -62,11 +62,12 @@ private async Task LogResponse(HttpContext context) await _next(context); - context.Response.Body.Seek(0, SeekOrigin.Begin); - using var reader = new StreamReader(context.Response.Body); string body = null; - if (reader.BaseStream.Length < maxStreamLength) + if (context.Response.ContentLength < maxStreamLength) { + context.Response.Body.Seek(0, SeekOrigin.Begin); + using var reader = new StreamReader(context.Response.Body); + body = await reader.ReadToEndAsync(); } context.Response.Body.Seek(0, SeekOrigin.Begin); diff --git a/source/backend/dal/Helpers/Extensions/PropertyExtensions.cs b/source/backend/dal/Helpers/Extensions/PropertyExtensions.cs index b0cd1394fc..9565ca551e 100644 --- a/source/backend/dal/Helpers/Extensions/PropertyExtensions.cs +++ b/source/backend/dal/Helpers/Extensions/PropertyExtensions.cs @@ -27,6 +27,37 @@ public static string GetHistoricalNumbersAsString(this PimsProperty property) // Print item return string.Join("; ", groupedHistorical.Select(g => g.GetAsString())); } + + public static string GetPropertyName(this IFilePropertyEntity fileProperty) + { + var property = fileProperty?.Property; + if(property == null) + { + return string.Empty; + } + + if (property.Pid.HasValue && property?.Pid.Value.ToString().Length > 0 && property?.Pid != '0') + { + return $"{property.Pid:000-000-000}"; + } + else if (property.Pin.HasValue && property?.Pin.Value.ToString()?.Length > 0 && property?.Pin != '0') + { + return property.Pin.ToString(); + } + else if (property?.SurveyPlanNumber != null && property?.SurveyPlanNumber.Length > 0) + { + return property.SurveyPlanNumber; + } + else if (property?.Location != null) + { + return $"{property.Location.Coordinate.X}, {property.Location.Coordinate.Y}"; + } + else if (property?.Address != null) + { + return property.Address.FormatAddress(); + } + return string.Empty; + } } // Helper class to aggregate historical numbers by type. diff --git a/source/backend/dal/Pims.Dal.csproj b/source/backend/dal/Pims.Dal.csproj index 3a76841cb6..d324f41777 100644 --- a/source/backend/dal/Pims.Dal.csproj +++ b/source/backend/dal/Pims.Dal.csproj @@ -11,6 +11,7 @@ + diff --git a/source/backend/dal/Repositories/DocumentQueueRepository.cs b/source/backend/dal/Repositories/DocumentQueueRepository.cs index 7ef007241a..463fbd8f2f 100644 --- a/source/backend/dal/Repositories/DocumentQueueRepository.cs +++ b/source/backend/dal/Repositories/DocumentQueueRepository.cs @@ -1,13 +1,17 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; +using DocumentFormat.OpenXml.Office2010.Excel; +using Medallion.Threading; +using Medallion.Threading.SqlServer; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Pims.Api.Models.CodeTypes; using Pims.Core.Extensions; using Pims.Dal.Entities; using Pims.Dal.Entities.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; namespace Pims.Dal.Repositories { @@ -16,6 +20,7 @@ namespace Pims.Dal.Repositories /// public class DocumentQueueRepository : BaseRepository, IDocumentQueueRepository { + private readonly IDistributedLockProvider _synchronizationProvider; #region Constructors /// @@ -27,9 +32,11 @@ public class DocumentQueueRepository : BaseRepository, IDocumentQu public DocumentQueueRepository( PimsContext dbContext, ClaimsPrincipal user, + IDistributedLockProvider synchronizationProvider, ILogger logger) : base(dbContext, user, logger) { + this._synchronizationProvider = synchronizationProvider; } #endregion @@ -42,7 +49,6 @@ public DocumentQueueRepository( /// public PimsDocumentQueue TryGetById(long documentQueueId) { - return Context.PimsDocumentQueues .AsNoTracking() .FirstOrDefault(dq => dq.DocumentQueueId == documentQueueId); @@ -83,25 +89,35 @@ public PimsDocumentQueue GetByDocumentId(long documentId) /// /// /// - public PimsDocumentQueue Update(PimsDocumentQueue queuedDocument, bool removeDocument = false) + public async Task Update(PimsDocumentQueue queuedDocument, bool removeDocument = false) { - queuedDocument.ThrowIfNull(nameof(queuedDocument)); - var existingQueuedDocument = TryGetById(queuedDocument.DocumentQueueId) ?? throw new KeyNotFoundException($"DocumentQueueId {queuedDocument.DocumentQueueId} not found."); - if (existingQueuedDocument?.DocumentQueueStatusTypeCode == DocumentQueueStatusTypes.SUCCESS.ToString() && queuedDocument.DocumentQueueStatusTypeCode != DocumentQueueStatusTypes.SUCCESS.ToString()) - { - throw new InvalidOperationException($"DocumentQueueId {queuedDocument.DocumentQueueId} is already completed."); - } - if (!removeDocument) + // Use a distributed lock to ensure that only one process can update the document queue at a time (prevents deadlocks from history triggers). + var @lock = this._synchronizationProvider.CreateLock("DocumentQueueLock"); + await using (await @lock.AcquireAsync()) { - queuedDocument.Document = existingQueuedDocument.Document; + queuedDocument.ThrowIfNull(nameof(queuedDocument)); + var existingQueuedDocument = Context.PimsDocumentQueues + .AsNoTracking() + .FirstOrDefault(dq => dq.DocumentQueueId == queuedDocument.DocumentQueueId) + ?? throw new KeyNotFoundException($"DocumentQueueId {queuedDocument.DocumentQueueId} not found."); + + if (existingQueuedDocument?.DocumentQueueStatusTypeCode == DocumentQueueStatusTypes.SUCCESS.ToString() && queuedDocument.DocumentQueueStatusTypeCode != DocumentQueueStatusTypes.SUCCESS.ToString()) + { + throw new InvalidOperationException($"DocumentQueueId {queuedDocument.DocumentQueueId} is already completed."); + } + + if (!removeDocument) + { + queuedDocument.Document = existingQueuedDocument.Document; + } + + queuedDocument.MayanError = queuedDocument.MayanError?.Truncate(4000); + queuedDocument.DataSourceTypeCode = existingQueuedDocument.DataSourceTypeCode; // Do not allow the data source to be updated. + Context.Entry(existingQueuedDocument).CurrentValues.SetValues(queuedDocument); + queuedDocument = Context.Update(queuedDocument).Entity; + await Context.SaveChangesAsync(); // Force changes to be saved here, in the scope of the lock. + return queuedDocument; } - - queuedDocument.MayanError = queuedDocument.MayanError?.Truncate(4000); - queuedDocument.DataSourceTypeCode = existingQueuedDocument.DataSourceTypeCode; // Do not allow the data source to be updated. - Context.Entry(existingQueuedDocument).CurrentValues.SetValues(queuedDocument); - queuedDocument = Context.Update(queuedDocument).Entity; - - return queuedDocument; } /// @@ -187,7 +203,7 @@ public int DocumentQueueCount(PimsDocumentQueueStatusType pimsDocumentQueueStatu { if (pimsDocumentQueueStatusType == null) { - Context.PimsDocumentQueues.Count(); + return Context.PimsDocumentQueues.Count(); } return Context.PimsDocumentQueues.Count(d => d.DocumentQueueStatusTypeCode == pimsDocumentQueueStatusType.DocumentQueueStatusTypeCode); diff --git a/source/backend/dal/Repositories/Interfaces/IDocumentQueueRepository.cs b/source/backend/dal/Repositories/Interfaces/IDocumentQueueRepository.cs index 5e57420673..319a7cbe52 100644 --- a/source/backend/dal/Repositories/Interfaces/IDocumentQueueRepository.cs +++ b/source/backend/dal/Repositories/Interfaces/IDocumentQueueRepository.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Threading.Tasks; using Pims.Dal.Entities; using Pims.Dal.Entities.Models; @@ -18,7 +19,7 @@ public interface IDocumentQueueRepository : IRepository IEnumerable GetAllByFilter(DocumentQueueFilter filter); - PimsDocumentQueue Update(PimsDocumentQueue queuedDocument, bool removeDocument = false); + Task Update(PimsDocumentQueue queuedDocument, bool removeDocument = false); bool Delete(PimsDocumentQueue queuedDocument); diff --git a/source/backend/entities/Partials/Property.cs b/source/backend/entities/Partials/Property.cs index cd8aab170b..82d862f509 100644 --- a/source/backend/entities/Partials/Property.cs +++ b/source/backend/entities/Partials/Property.cs @@ -48,7 +48,7 @@ public PimsProperty() /// public PimsProperty(int pid, PimsPropertyType type, PimsAddress address, PimsPropPropTenureTyp tenure, PimsAreaUnitType areaUnit, PimsDataSourceType dataSource, DateTime dataSourceEffectiveDate, PimsPropertyStatusType status) { - this.Pid = pid; + this.Pid = pid > 0 ? pid : null; this.PropertyTypeCodeNavigation = type ?? throw new ArgumentNullException(nameof(type)); this.PropertyTypeCode = type.Id; this.Address = address ?? throw new ArgumentNullException(nameof(address)); diff --git a/source/backend/entities/ef/PimsPropertyBoundaryResearchVw.cs b/source/backend/entities/ef/PimsPropertyBoundaryResearchVw.cs index 90dbf15693..c2aabe40a4 100644 --- a/source/backend/entities/ef/PimsPropertyBoundaryResearchVw.cs +++ b/source/backend/entities/ef/PimsPropertyBoundaryResearchVw.cs @@ -13,6 +13,6 @@ public partial class PimsPropertyBoundaryResearchVw [Column("PROPERTY_ID")] public long PropertyId { get; set; } - [Column("LOCATION", TypeName = "geometry")] - public Geometry Location { get; set; } + [Column("BOUNDARY", TypeName = "geometry")] + public Geometry Boundary { get; set; } } diff --git a/source/backend/geocoder/Parameters/AddressesParameters.cs b/source/backend/geocoder/Parameters/AddressesParameters.cs index ab0e729cd8..2592cfa92c 100644 --- a/source/backend/geocoder/Parameters/AddressesParameters.cs +++ b/source/backend/geocoder/Parameters/AddressesParameters.cs @@ -1,46 +1,69 @@ namespace Pims.Geocoder.Parameters { /// - /// AddressesParameters class, provides a way to pass parameters to the addresses endpoint. + /// + /// AddressesParameters class, provides a way to pass parameters to the addresses endpoint of BC Address Geocoder. + /// + /// See - . /// public class AddressesParameters : BaseParameters { #region Properties /// - /// get/set - The API version. - /// - public string Ver { get; set; } = "1.2"; - - /// + /// /// get/set - Civic address or intersection address as a single string in Single-line Address Format. If not present in an address request, individual address elements, such as streetName, localityName, and provinceCode must be provided. /// In an occupant/addresses resource, addressString represents an Occupant name followed by a frontGate delimiter('--') followed by an optional address. + /// + /// + /// For example: + /// + /// 525 Superior Street, Victoria, BC + /// + /// /// public string AddressString { get; set; } /// /// get/set - The maximum number of search results to return. /// + /// Default value: 5. public int MaxResults { get; set; } = 5; /// - /// get/set - In the case of a block level match, the method of interpolation to determine how far down the block the accessPoint should be. The geocoder supports linear and adaptive interpolation. + /// get/set - In the case of a block level match, the method of interpolation to determine how far down the block the accessPoint should be. The geocoder supports none, linear and adaptive interpolation. /// + /// Default value: adaptive. public string Interpolation { get; set; } = "adaptive"; /// - /// get/set - If true, include unmatched address details such as site name in results. + /// get/set - If true, include unmatched address details such as site name in results. /// + /// Default value: true. public bool Echo { get; set; } /// - /// get/set - If true, addressString is expected to contain a partial address that requires completion. Not supported for shp, csv, gml formats. + /// get/set - If true, addressString is expected to contain a partial address that requires completion. Not supported for shp, csv, gml formats. + /// + /// Default value: true. + public bool AutoComplete { get; set; } = true; + + /// + /// get/set - If true, autoComplete suggestions are limited to addresses beginning with the provided partial address. + /// + /// Default value: true. + public bool ExactSpelling { get; set; } = true; + + /// + /// get/set - If true, autoComplete suggestions are sorted using a fuzzy match comparison to the addressString. /// - public bool AutoComplete { get; set; } + /// Default value: false. + public bool FuzzyMatch { get; set; } /// /// get/set - The minimum score required for a match to be returned. /// + /// Default value: 1. public int MinScore { get; set; } /// @@ -126,7 +149,8 @@ public class AddressesParameters : BaseParameters /// /// get/set - The ISO 3166-2 Sub-Country Code. The code for British Columbia is BC. /// - public string ProvinceCode { get; set; } + /// Default value: BC. + public string ProvinceCode { get; set; } = "BC"; /// /// get/set - A comma separated list of locality names that matched addresses must belong to. For example, setting localities to Nanaimo only returns addresses in Nanaimo. @@ -149,7 +173,7 @@ public class AddressesParameters : BaseParameters public string Center { get; set; } /// - /// get/set - If true, uses supplied parcelPoint to derive an appropriate accessPoint. + /// get/set - If true, uses supplied parcelPoint to derive an appropriate accessPoint. /// public bool Extrapolate { get; set; } diff --git a/source/backend/geocoder/Parameters/BaseParameters.cs b/source/backend/geocoder/Parameters/BaseParameters.cs index 02b0e6de49..a121fbfe44 100644 --- a/source/backend/geocoder/Parameters/BaseParameters.cs +++ b/source/backend/geocoder/Parameters/BaseParameters.cs @@ -23,6 +23,7 @@ public abstract class BaseParameters /// /// get/set - The distance to move the accessPoint away from the curb and towards the inside of the parcel (in metres). Ignored if locationDescriptor not set to accessPoint. /// + /// Default value: 0. public int SetBack { get; set; } /// @@ -31,8 +32,9 @@ public abstract class BaseParameters public virtual double? MaxDistance { get; set; } /// - /// get/set - If true, include only basic match and address details in results. Not supported for shp, csv, and gml formats. + /// get/set - If true, include only basic match and address details in results. Not supported for shp, csv, and gml formats. /// + /// Default value: false. public bool Brief { get; set; } } } diff --git a/source/backend/ltsa/LtsaService.cs b/source/backend/ltsa/LtsaService.cs index a289e2e12a..f2e2822c6c 100644 --- a/source/backend/ltsa/LtsaService.cs +++ b/source/backend/ltsa/LtsaService.cs @@ -328,8 +328,9 @@ public async Task>> PostParcelInfoOrder(str /// public async Task>> PostSpcpOrder(string strataPlanNumber) { - SpcpOrder order = new(new StrataPlanCommonPropertyOrderParameters(strataPlanNumber)); - var url = this.Options.HostUri.AppendToURL(this.Options.OrdersEndpoint); + var url = Options.HostUri.AppendToURL(Options.OrdersEndpoint); + SpcpOrder order = new(new StrataPlanCommonPropertyOrderParameters(strataPlanNumber), productType: OrderParent.ProductTypeEnum.commonProperty); + return await PostOrderAsync>(url, new OrderWrapper(order)); } diff --git a/source/backend/scheduler/Services/DocumentQueueService.cs b/source/backend/scheduler/Services/DocumentQueueService.cs index 5f1bf9e14b..f8dd52e3d9 100644 --- a/source/backend/scheduler/Services/DocumentQueueService.cs +++ b/source/backend/scheduler/Services/DocumentQueueService.cs @@ -41,13 +41,19 @@ public DocumentQueueService( public async Task UploadQueuedDocuments() { - var filter = new DocumentQueueFilter() { Quantity = _uploadQueuedDocumentsJobOptions?.CurrentValue?.BatchSize ?? 50, DocumentQueueStatusTypeCodes = new string[] { DocumentQueueStatusTypes.PENDING.ToString() }, MaxFileSize = _uploadQueuedDocumentsJobOptions?.CurrentValue?.MaxFileSize }; + var filter = new DocumentQueueFilter() { Quantity = _uploadQueuedDocumentsJobOptions?.CurrentValue?.BatchSize ?? 50, DocumentQueueStatusTypeCodes = new string[] { DocumentQueueStatusTypes.PENDING.ToString(), DocumentQueueStatusTypes.PROCESSING.ToString() }, MaxFileSize = _uploadQueuedDocumentsJobOptions?.CurrentValue?.MaxFileSize }; var searchResponse = await SearchQueuedDocuments(filter); if (searchResponse?.ScheduledTaskResponseModel != null) { return searchResponse?.ScheduledTaskResponseModel; } + if(searchResponse?.SearchResults?.Payload?.Any(s => s.DocumentQueueStatusType.Id == DocumentQueueStatusTypes.PROCESSING.ToString()) == true) + { + _logger.LogInformation("There are still documents that are actively being processed, skipping upload."); + return new ScheduledTaskResponseModel() { Status = TaskResponseStatusTypes.SKIPPED, Message = "There are still documents that are actively being processed, skipping upload." }; + } + IEnumerable> responses = searchResponse?.SearchResults?.Payload?.Select(qd => { _logger.LogInformation("Uploading Queued document {documentQueueId}", qd.Id); diff --git a/source/backend/tests/api/Services/DocumentQueueServiceTest.cs b/source/backend/tests/api/Services/DocumentQueueServiceTest.cs index a6704c8c2a..4b165da80f 100644 --- a/source/backend/tests/api/Services/DocumentQueueServiceTest.cs +++ b/source/backend/tests/api/Services/DocumentQueueServiceTest.cs @@ -128,7 +128,7 @@ public void SearchDocumentQueue_InvalidPermissions_ThrowsNotAuthorizedException( } [Fact] - public void Update_Success() + public async void Update_Success() { // Arrange var service = CreateDocumentQueueServiceWithPermissions(Permissions.SystemAdmin); @@ -139,12 +139,11 @@ public void Update_Success() documentQueueRepositoryMock.Setup(m => m.CommitTransaction()); // Act - var result = service.Update(documentQueue); + var result = await service.Update(documentQueue); // Assert result.Should().Be(documentQueue); documentQueueRepositoryMock.Verify(m => m.Update(documentQueue, false), Times.Once); - documentQueueRepositoryMock.Verify(m => m.CommitTransaction(), Times.Once); } [Fact] @@ -155,10 +154,10 @@ public void Update_InvalidPermissions_ThrowsNotAuthorizedException() var documentQueue = new PimsDocumentQueue { DocumentQueueId = 1 }; // Act - Action act = () => service.Update(documentQueue); + Func act = async () => await service.Update(documentQueue); // Assert - act.Should().Throw(); + act.Should().ThrowAsync(); } [Fact] @@ -213,12 +212,11 @@ public async Task PollForDocument_RelatedDocumentMayanIdNull_UpdatesStatusToPIMS documentQueueRepositoryMock.Setup(m => m.TryGetById(documentQueue.DocumentQueueId)).Returns(databaseDocumentQueue); // Act - var result = await service.PollForDocument(documentQueue); + Func act = async () => await service.PollForDocument(documentQueue); // Assert - result.Should().Be(databaseDocumentQueue); + await act.Should().ThrowAsync(); documentQueueRepositoryMock.Verify(m => m.Update(databaseDocumentQueue, false), Times.Once); - documentQueueRepositoryMock.Verify(m => m.CommitTransaction(), Times.Once); } [Fact] @@ -244,7 +242,6 @@ public async Task PollForDocument_GetStorageDocumentDetailFails_UpdatesStatusToP // Assert result.Should().Be(databaseDocumentQueue); documentQueueRepositoryMock.Verify(m => m.Update(databaseDocumentQueue, false), Times.Once); - documentQueueRepositoryMock.Verify(m => m.CommitTransaction(), Times.Once); } [Fact] @@ -254,7 +251,7 @@ public async Task PollForDocument_FileLatestIdNull_LogsFileStillProcessing() var service = CreateDocumentQueueServiceWithPermissions(Permissions.SystemAdmin); var documentQueue = new PimsDocumentQueue { DocumentQueueId = 1, DocumentId = 1 }; var relatedDocument = new PimsDocument { MayanId = 1 }; - var databaseDocumentQueue = new PimsDocumentQueue { DocumentQueueId = 1 }; + var databaseDocumentQueue = new PimsDocumentQueue { DocumentQueueId = 1, DocumentQueueStatusTypeCode = DocumentQueueStatusTypes.PROCESSING.ToString() }; var documentDetailModel = new DocumentDetailModel { FileLatest = null }; var documentDetailsResponse = new ExternalResponse { Status = ExternalResponseStatus.Success, Payload = documentDetailModel }; var documentRepositoryMock = this._helper.GetService>(); @@ -299,7 +296,6 @@ public async Task PollForDocument_FileLatestIdNotNull_UpdatesStatusToSuccess() result.Should().Be(databaseDocumentQueue); result.DocumentQueueStatusTypeCode.Should().Be(DocumentQueueStatusTypes.SUCCESS.ToString()); documentQueueRepositoryMock.Verify(m => m.Update(databaseDocumentQueue, true), Times.Once); - documentQueueRepositoryMock.Verify(m => m.CommitTransaction(), Times.Once); } [Fact] @@ -364,7 +360,6 @@ public async Task Upload_Success() result.Should().NotBeNull(); result.DocumentQueueStatusTypeCode.Should().Be(DocumentQueueStatusTypes.SUCCESS.ToString()); documentQueueRepositoryMock.Verify(x => x.Update(It.IsAny(), It.IsAny()), Times.AtLeastOnce); - documentQueueRepositoryMock.Verify(x => x.CommitTransaction(), Times.AtLeastOnce); documentServiceMock.Verify(x => x.UploadDocumentAsync(It.IsAny(), true), Times.Once); } @@ -431,7 +426,6 @@ public async Task Upload_Retry_Success() result.DocProcessRetries.Should().Be(1); result.DocumentQueueStatusTypeCode.Should().Be(DocumentQueueStatusTypes.SUCCESS.ToString()); documentQueueRepositoryMock.Verify(x => x.Update(It.IsAny(), It.IsAny()), Times.AtLeastOnce); - documentQueueRepositoryMock.Verify(x => x.CommitTransaction(), Times.AtLeastOnce); documentServiceMock.Verify(x => x.UploadDocumentAsync(It.IsAny(), true), Times.Once); } @@ -462,7 +456,6 @@ public async Task Upload_ValidateQueuedDocumentFails_UpdatesStatusToPIMSError() result.Should().NotBeNull(); result.DocumentQueueStatusTypeCode.Should().Be(DocumentQueueStatusTypes.PIMS_ERROR.ToString()); documentQueueRepositoryMock.Verify(x => x.Update(It.IsAny(), It.IsAny()), Times.AtLeastOnce); - documentQueueRepositoryMock.Verify(x => x.CommitTransaction(), Times.AtLeastOnce); documentServiceMock.Verify(x => x.UploadDocumentAsync(It.IsAny(), true), Times.Never); } @@ -599,7 +592,6 @@ public async Task Upload_RelatedDocument_MayanId() result.Should().NotBeNull(); result.DocumentQueueStatusTypeCode.Should().Be(DocumentQueueStatusTypes.PROCESSING.ToString()); documentQueueRepositoryMock.Verify(x => x.Update(It.IsAny(), It.IsAny()), Times.AtLeastOnce); - documentQueueRepositoryMock.Verify(x => x.CommitTransaction(), Times.AtLeastOnce); documentServiceMock.Verify(x => x.UploadDocumentAsync(It.IsAny(), true), Times.Once); } @@ -688,13 +680,11 @@ public async Task Upload_DocumentTypeIdNull() documentRepositoryMock.Setup(x => x.TryGetDocumentRelationships(It.IsAny())).Returns((PimsDocument)null); // Act - var result = await service.Upload(documentQueue); + Func act = async () => await service.Upload(documentQueue); + await act.Should().ThrowAsync(); // Assert - result.Should().NotBeNull(); - result.DocumentQueueStatusTypeCode.Should().Be(DocumentQueueStatusTypes.PIMS_ERROR.ToString()); - documentQueueRepositoryMock.Verify(x => x.Update(It.IsAny(), It.IsAny()), Times.AtLeastOnce); - documentQueueRepositoryMock.Verify(x => x.CommitTransaction(), Times.AtLeastOnce); + documentQueueRepositoryMock.Verify(x => x.Update(It.Is(p => p.DocumentQueueStatusTypeCode == DocumentQueueStatusTypes.PIMS_ERROR.ToString()), It.IsAny()), Times.AtLeastOnce); } [Fact] @@ -752,7 +742,6 @@ public async Task Upload_UploadDocumentFails_UpdatesStatusToMayanError() result.Should().NotBeNull(); result.DocumentQueueStatusTypeCode.Should().Be(DocumentQueueStatusTypes.MAYAN_ERROR.ToString()); documentQueueRepositoryMock.Verify(x => x.Update(It.IsAny(), It.IsAny()), Times.AtLeastOnce); - documentQueueRepositoryMock.Verify(x => x.CommitTransaction(), Times.AtLeastOnce); documentServiceMock.Verify(x => x.UploadDocumentAsync(It.IsAny(), true), Times.Once); } diff --git a/source/backend/tests/api/Services/ManagementFileServiceTest.cs b/source/backend/tests/api/Services/ManagementFileServiceTest.cs index a564161781..8c4997e41d 100644 --- a/source/backend/tests/api/Services/ManagementFileServiceTest.cs +++ b/source/backend/tests/api/Services/ManagementFileServiceTest.cs @@ -367,7 +367,7 @@ public void Update_Success_Team_SameRole() var service = this.CreateManagementServiceWithPermissions(Permissions.ManagementEdit); var repository = this._helper.GetService>(); - var statusMock = this._helper.GetService>(); + var statusMock = this._helper.GetService>(); statusMock.Setup(x => x.CanEditDetails(It.IsAny())).Returns(true); var managementFile = EntityHelper.CreateManagementFile(1); @@ -394,7 +394,7 @@ public void Update_Should_Fail_Duplicate_Team() var service = this.CreateManagementServiceWithPermissions(Permissions.ManagementEdit); var repository = this._helper.GetService>(); - var statusMock = this._helper.GetService>(); + var statusMock = this._helper.GetService>(); statusMock.Setup(x => x.CanEditDetails(It.IsAny())).Returns(true); var managementFile = EntityHelper.CreateManagementFile(1); @@ -420,7 +420,7 @@ public void Update_Should_Fail_Complete_NonInventoryProperty() var repository = this._helper.GetService>(); var managementFilePropertyRepository = this._helper.GetService>(); - var statusMock = this._helper.GetService>(); + var statusMock = this._helper.GetService>(); statusMock.Setup(x => x.CanEditDetails(It.IsAny())).Returns(true); var managementFile = EntityHelper.CreateManagementFile(1); @@ -469,7 +469,7 @@ public void Update_Should_Fail_Final(ManagementFileStatusTypes fileStatus) repository.Setup(x => x.GetById(It.IsAny())).Returns(managementFile); repository.Setup(x => x.GetByName(It.IsAny())).Returns(managementFile); - var statusMock = this._helper.GetService>(); + var statusMock = this._helper.GetService>(); statusMock.Setup(x => x.CanEditDetails(It.IsAny())).Returns(false); // Act @@ -498,7 +498,7 @@ public void Update_Final_Validation_Success(ManagementFileStatusTypes fileStatus var managementFile = EntityHelper.CreateManagementFile(1); managementFile.ManagementFileStatusTypeCode = fileStatus.ToString(); - var statusMock = this._helper.GetService>(); + var statusMock = this._helper.GetService>(); statusMock.Setup(x => x.CanEditDetails(It.IsAny())).Returns(true); repository.Setup(x => x.GetRowVersion(It.IsAny())).Returns(1); @@ -521,7 +521,7 @@ public void Update_Success() var service = this.CreateManagementServiceWithPermissions(Permissions.ManagementEdit); var repository = this._helper.GetService>(); - var statusMock = this._helper.GetService>(); + var statusMock = this._helper.GetService>(); statusMock.Setup(x => x.CanEditDetails(It.IsAny())).Returns(true); var managementFile = EntityHelper.CreateManagementFile(1); @@ -554,7 +554,7 @@ public void Update_Success_FinalButAdmin() repository.Setup(x => x.GetById(It.IsAny())).Returns(managementFile); repository.Setup(x => x.GetByName(It.IsAny())).Returns(managementFile); - var statusMock = this._helper.GetService>(); + var statusMock = this._helper.GetService>(); statusMock.Setup(x => x.CanEditDetails(It.IsAny())).Returns(false); // Act @@ -579,7 +579,7 @@ public void Update_Success_AddsNote() var noteRepository = this._helper.GetService>>(); var lookupRepository = this._helper.GetService>(); - var statusMock = this._helper.GetService>(); + var statusMock = this._helper.GetService>(); statusMock.Setup(x => x.CanEditDetails(It.IsAny())).Returns(true); repository.Setup(x => x.Update(It.IsAny(), It.IsAny())).Returns(managementFile); @@ -626,7 +626,7 @@ public void UpdateProperties_Success() var propertyActivityRepository = this._helper.GetService>(); propertyActivityRepository.Setup(x => x.GetActivitiesByManagementFile(It.IsAny())).Returns(new List()); - var statusMock = this._helper.GetService>(); + var statusMock = this._helper.GetService>(); statusMock.Setup(x => x.GetCurrentManagementStatus(It.IsAny())).Returns(ManagementFileStatusTypes.ACTIVE); statusMock.Setup(x => x.CanEditProperties(It.IsAny())).Returns(true); @@ -664,7 +664,7 @@ public void UpdateProperties_MatchProperties_Success() propertyService.Setup(x => x.UpdateLocation(It.IsAny(), ref It.Ref.IsAny, It.IsAny>(), false)); propertyService.Setup(x => x.UpdateFilePropertyLocation(It.IsAny(), It.IsAny())); - var statusMock = this._helper.GetService>(); + var statusMock = this._helper.GetService>(); statusMock.Setup(x => x.GetCurrentManagementStatus(It.IsAny())).Returns(ManagementFileStatusTypes.ACTIVE); statusMock.Setup(x => x.CanEditProperties(It.IsAny())).Returns(true); @@ -709,7 +709,7 @@ public void UpdateProperties_MatchProperties_Success_NoInternalId() propertyService.Setup(x => x.UpdateLocation(It.IsAny(), ref It.Ref.IsAny, It.IsAny>(), false)); propertyService.Setup(x => x.UpdateFilePropertyLocation(It.IsAny(), It.IsAny())); - var statusMock = this._helper.GetService>(); + var statusMock = this._helper.GetService>(); statusMock.Setup(x => x.GetCurrentManagementStatus(It.IsAny())).Returns(ManagementFileStatusTypes.ACTIVE); statusMock.Setup(x => x.CanEditProperties(It.IsAny())).Returns(true); @@ -761,7 +761,7 @@ public void UpdateProperties_MatchProperties_NewProperty_UserOverride() }); propertyService.Setup(x => x.PopulateNewFileProperty(It.IsAny())).Returns(x => x); - var statusMock = this._helper.GetService>(); + var statusMock = this._helper.GetService>(); statusMock.Setup(x => x.GetCurrentManagementStatus(It.IsAny())).Returns(ManagementFileStatusTypes.ACTIVE); statusMock.Setup(x => x.CanEditProperties(It.IsAny())).Returns(true); @@ -810,7 +810,7 @@ public void UpdateProperties_MatchProperties_NewProperty_Success() }); propertyService.Setup(x => x.PopulateNewFileProperty(It.IsAny())).Returns(x => x); - var statusMock = this._helper.GetService>(); + var statusMock = this._helper.GetService>(); statusMock.Setup(x => x.GetCurrentManagementStatus(It.IsAny())).Returns(ManagementFileStatusTypes.ACTIVE); statusMock.Setup(x => x.CanEditProperties(It.IsAny())).Returns(true); @@ -864,7 +864,7 @@ public void UpdateProperties_UpdatePropertyFile_Success() var propertyService = this._helper.GetService>(); - var statusMock = this._helper.GetService>(); + var statusMock = this._helper.GetService>(); statusMock.Setup(x => x.GetCurrentManagementStatus(It.IsAny())).Returns(ManagementFileStatusTypes.ACTIVE); statusMock.Setup(x => x.CanEditProperties(It.IsAny())).Returns(true); @@ -902,7 +902,7 @@ public void UpdateProperties_RemovePropertyFile_Success() var propertyActivityRepository = this._helper.GetService>(); propertyActivityRepository.Setup(x => x.GetActivitiesByManagementFile(It.IsAny())).Returns(new List()); - var statusMock = this._helper.GetService>(); + var statusMock = this._helper.GetService>(); statusMock.Setup(x => x.GetCurrentManagementStatus(It.IsAny())).Returns(ManagementFileStatusTypes.ACTIVE); statusMock.Setup(x => x.CanEditProperties(It.IsAny())).Returns(true); @@ -949,7 +949,7 @@ public void UpdateProperties_RemoveProperty_Success() var propertyActivityRepository = this._helper.GetService>(); propertyActivityRepository.Setup(x => x.GetActivitiesByManagementFile(It.IsAny())).Returns(new List()); - var statusMock = this._helper.GetService>(); + var statusMock = this._helper.GetService>(); statusMock.Setup(x => x.GetCurrentManagementStatus(It.IsAny())).Returns(ManagementFileStatusTypes.ACTIVE); statusMock.Setup(x => x.CanEditProperties(It.IsAny())).Returns(true); @@ -996,7 +996,7 @@ public void UpdateProperties_RemoveProperty_Fails_PropertyIsSubdividedOrConsolid var propertyActivityRepository = this._helper.GetService>(); propertyActivityRepository.Setup(x => x.GetActivitiesByManagementFile(It.IsAny())).Returns(new List()); - var statusMock = this._helper.GetService>(); + var statusMock = this._helper.GetService>(); statusMock.Setup(x => x.GetCurrentManagementStatus(It.IsAny())).Returns(ManagementFileStatusTypes.ACTIVE); statusMock.Setup(x => x.CanEditProperties(It.IsAny())).Returns(true); @@ -1053,7 +1053,7 @@ public void UpdateProperties_ValidatePropertyRegions_Success() var propertyActivityRepository = this._helper.GetService>(); propertyActivityRepository.Setup(x => x.GetActivitiesByManagementFile(It.IsAny())).Returns(new List()); - var statusMock = this._helper.GetService>(); + var statusMock = this._helper.GetService>(); statusMock.Setup(x => x.GetCurrentManagementStatus(It.IsAny())).Returns(ManagementFileStatusTypes.ACTIVE); statusMock.Setup(x => x.CanEditProperties(It.IsAny())).Returns(true); @@ -1064,6 +1064,48 @@ public void UpdateProperties_ValidatePropertyRegions_Success() filePropertyRepository.Verify(x => x.GetPropertiesByManagementFileId(It.IsAny()), Times.Once); } + [Fact] + public void UpdateProperties_DisableProperties_AddsNote() + { + // Arrange + var service = this.CreateManagementServiceWithPermissions(Permissions.ManagementEdit, Permissions.PropertyAdd, Permissions.PropertyView); + + var managementFile = EntityHelper.CreateManagementFile(); + managementFile.ConcurrencyControlNumber = 1; + + var property = EntityHelper.CreateProperty(12345, regionCode: 1); + managementFile.PimsManagementFileProperties = new List() { new PimsManagementFileProperty() { Property = property, Internal_Id = 1, IsActive = true } }; + + var repository = this._helper.GetService>(); + repository.Setup(x => x.GetRowVersion(It.IsAny())).Returns(1); + repository.Setup(x => x.GetById(It.IsAny())).Returns(managementFile); + + var propertyRepository = this._helper.GetService>(); + propertyRepository.Setup(x => x.GetByPid(It.IsAny(), true)).Returns(property); + propertyRepository.Setup(x => x.GetPropertyRegion(It.IsAny())).Returns(1); + + var filePropertyRepository = this._helper.GetService>(); + filePropertyRepository.Setup(x => x.GetPropertiesByManagementFileId(It.IsAny())).Returns(new List() { new PimsManagementFileProperty() { Property = property, Internal_Id = 1, IsActive = false } }); + + var propertyActivityRepository = this._helper.GetService>(); + propertyActivityRepository.Setup(x => x.GetActivitiesByManagementFile(It.IsAny())).Returns(new List()); + + var statusMock = this._helper.GetService>(); + statusMock.Setup(x => x.GetCurrentManagementStatus(It.IsAny())).Returns(ManagementFileStatusTypes.ACTIVE); + statusMock.Setup(x => x.CanEditProperties(It.IsAny())).Returns(true); + + List note = new List(); + var entityNoteRepository = this._helper.GetService>>(); + entityNoteRepository.Setup(x => x.AddNoteRelationship(Capture.In(note))).Returns(new PimsManagementFileNote()); + + // Act + service.UpdateProperties(managementFile, new List()); + + // Assert + entityNoteRepository.Verify(x => x.AddNoteRelationship(It.IsAny()), Times.Once); + note.FirstOrDefault().Note.NoteTxt.Should().Be("Management File property 000-012-345 Enabled"); + } + [Fact] public void UpdateProperties_FinalFile_Error() { @@ -1087,7 +1129,7 @@ public void UpdateProperties_FinalFile_Error() var filePropertyRepository = this._helper.GetService>(); filePropertyRepository.Setup(x => x.GetPropertiesByManagementFileId(It.IsAny())).Returns(managementFile.PimsManagementFileProperties.ToList()); - var statusMock = this._helper.GetService>(); + var statusMock = this._helper.GetService>(); statusMock.Setup(x => x.GetCurrentManagementStatus(It.IsAny())).Returns(ManagementFileStatusTypes.ACTIVE); statusMock.Setup(x => x.CanEditProperties(It.IsAny())).Returns(false); @@ -1129,7 +1171,7 @@ public void UpdateProperties_WithRetiredProperty_Should_Fail() var propertyRepository = this._helper.GetService>(); propertyRepository.Setup(x => x.GetByPid(It.IsAny(), true)).Returns(retiredProperty); - var statusMock = this._helper.GetService>(); + var statusMock = this._helper.GetService>(); statusMock.Setup(x => x.GetCurrentManagementStatus(It.IsAny())).Returns(ManagementFileStatusTypes.ACTIVE); statusMock.Setup(x => x.CanEditProperties(It.IsAny())).Returns(true); @@ -1187,7 +1229,7 @@ public void UpdateProperties_WithManagementPropertyActivity_Should_Fail() var propertyOperationsService = this._helper.GetService>(); propertyOperationsService.Setup(x => x.GetOperationsForProperty(It.IsAny())).Returns(new List()); - var statusMock = this._helper.GetService>(); + var statusMock = this._helper.GetService>(); statusMock.Setup(x => x.GetCurrentManagementStatus(It.IsAny())).Returns(ManagementFileStatusTypes.ACTIVE); statusMock.Setup(x => x.CanEditProperties(It.IsAny())).Returns(true); diff --git a/source/backend/tests/api/Solvers/AcquisitionStatusSolverTests.cs b/source/backend/tests/api/Solvers/AcquisitionStatusSolverTests.cs index 5ec73076db..698488c904 100644 --- a/source/backend/tests/api/Solvers/AcquisitionStatusSolverTests.cs +++ b/source/backend/tests/api/Solvers/AcquisitionStatusSolverTests.cs @@ -214,7 +214,7 @@ public void CanEditCompensations_Parametrized(AcquisitionStatusTypes? status, bo new object[] {AcquisitionStatusTypes.CANCEL, false}, new object[] {AcquisitionStatusTypes.CLOSED, false}, new object[] {AcquisitionStatusTypes.COMPLT, false}, - new object[] {AcquisitionStatusTypes.HOLD, true}, + new object[] {AcquisitionStatusTypes.HOLD, false}, }; [Theory] diff --git a/source/backend/tests/api/Solvers/ManagementFileStatusSolverTests.cs b/source/backend/tests/api/Solvers/ManagementFileStatusSolverTests.cs new file mode 100644 index 0000000000..56ae0daf44 --- /dev/null +++ b/source/backend/tests/api/Solvers/ManagementFileStatusSolverTests.cs @@ -0,0 +1,69 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Pims.Api.Models.CodeTypes; +using Pims.Api.Services; +using Xunit; + +namespace Pims.Api.Test.Solvers +{ + [Trait("category", "unit")] + [Trait("category", "api")] + [Trait("group", "management")] + [ExcludeFromCodeCoverage] + public class ManagementFileStatusSolverTests + { + public static IEnumerable CanEditDetailsParameters => + new List + { + new object[] {null, false}, + new object[] { ManagementFileStatusTypes.ACTIVE, true}, + new object[] { ManagementFileStatusTypes.DRAFT, true}, + new object[] { ManagementFileStatusTypes.THIRDRDPARTY, true}, + new object[] { ManagementFileStatusTypes.ARCHIVED, false}, + new object[] { ManagementFileStatusTypes.CANCELLED, false}, + new object[] { ManagementFileStatusTypes.COMPLETE, false}, + new object[] { ManagementFileStatusTypes.HOLD, false}, + }; + + public static IEnumerable IsAdminProtected => + new List + { + new object[] {null, false}, + new object[] { ManagementFileStatusTypes.ACTIVE, false}, + new object[] { ManagementFileStatusTypes.DRAFT, false}, + new object[] { ManagementFileStatusTypes.THIRDRDPARTY, false}, + new object[] { ManagementFileStatusTypes.CANCELLED, false}, + new object[] { ManagementFileStatusTypes.HOLD, false}, + new object[] { ManagementFileStatusTypes.ARCHIVED, true}, + new object[] { ManagementFileStatusTypes.COMPLETE, true}, + }; + + [Theory] + [MemberData(nameof(CanEditDetailsParameters))] + public void CanEditDetails_Parametrized(ManagementFileStatusTypes? status, bool expectedResult) + { + // Arrange + var solver = new ManagementFileStatusSolver(); + + // Act + var result = solver.CanEditDetails(status); + + // Assert + Assert.Equal(expectedResult, result); + } + + [Theory] + [MemberData(nameof(IsAdminProtected))] + public void IsAdminProtected_Parametrized(ManagementFileStatusTypes? status, bool expectedResult) + { + // Arrange + var solver = new ManagementFileStatusSolver(); + + // Act + var result = solver.IsAdminProtected(status); + + // Assert + Assert.Equal(expectedResult, result); + } + } +} diff --git a/source/backend/tests/core/Entities/PropertyHelper.cs b/source/backend/tests/core/Entities/PropertyHelper.cs index 37403877af..a76d2975ee 100644 --- a/source/backend/tests/core/Entities/PropertyHelper.cs +++ b/source/backend/tests/core/Entities/PropertyHelper.cs @@ -22,7 +22,7 @@ public static partial class EntityHelper /// /// /// - public static PimsProperty CreateProperty(int pid, int? pin = null, PimsPropertyType type = null, PimsAddress address = null, PimsPropertyTenureType tenure = null, PimsAreaUnitType areaUnit = null, PimsDataSourceType dataSource = null, PimsPropertyStatusType status = null, PimsLease lease = null, short? regionCode = null, bool? isCoreInventory = null, bool? isRetired = null) + public static PimsProperty CreateProperty(int pid, int? pin = null, PimsPropertyType type = null, PimsAddress address = null, PimsPropertyTenureType tenure = null, PimsAreaUnitType areaUnit = null, PimsDataSourceType dataSource = null, PimsPropertyStatusType status = null, PimsLease lease = null, short? regionCode = null, bool? isCoreInventory = null, bool? isRetired = null, string? surveyPlanNumber = null, bool? noLocation = null) { type ??= CreatePropertyType($"Land-{pid}"); address ??= CreateAddress(pid); @@ -37,9 +37,10 @@ public static PimsProperty CreateProperty(int pid, int? pin = null, PimsProperty PropertyId = pid, Pin = pin, ConcurrencyControlNumber = 1, - Location = new NetTopologySuite.Geometries.Point(0, 0) { SRID = SpatialReference.BCALBERS }, + Location = noLocation != true ? new NetTopologySuite.Geometries.Point(0, 0) { SRID = SpatialReference.BCALBERS } : null, SurplusDeclarationTypeCode = "SURPLUS", IsRetired = false, + SurveyPlanNumber = surveyPlanNumber, }; if (lease != null) diff --git a/source/backend/tests/dal/Libraries/Geocoder/GeocoderServiceTest.cs b/source/backend/tests/dal/Libraries/Geocoder/GeocoderServiceTest.cs index eda9525c16..786aad0a6f 100644 --- a/source/backend/tests/dal/Libraries/Geocoder/GeocoderServiceTest.cs +++ b/source/backend/tests/dal/Libraries/Geocoder/GeocoderServiceTest.cs @@ -88,8 +88,8 @@ public async Task GetSiteAddressesAsync_StringAddress_Success() // Act var result = await service.GetSiteAddressesAsync("address"); - // Assert - var url = "https://geocoder.api.gov.bc.ca/addresses?ver=1.2&addressString=address&maxResults=5&interpolation=adaptive&echo=false&autoComplete=false&minScore=0&maxDistance=0&extrapolate=false&outputSRS=4326&locationDescriptor=any&setBack=0&brief=false"; + // Assert + var url = "https://geocoder.api.gov.bc.ca/addresses?addressString=address&maxResults=5&interpolation=adaptive&echo=false&autoComplete=true&exactSpelling=true&fuzzyMatch=false&minScore=0&maxDistance=0&provinceCode=BC&extrapolate=false&outputSRS=4326&locationDescriptor=any&setBack=0&brief=false"; result.Should().NotBeNull(); mockRequestClient.Verify(m => m.GetAsync(url), Times.Once()); result.Should().Be(features); @@ -119,7 +119,7 @@ public async Task GetSiteAddressesAsync_StringAddress_Encoding_Success() var result = await service.GetSiteAddressesAsync("address with encoding"); // Assert - var url = "https://geocoder.api.gov.bc.ca/addresses.json?ver=1.2&addressString=address%2Bwith%2Bencoding&maxResults=5&interpolation=adaptive&echo=false&autoComplete=false&minScore=0&maxDistance=0&extrapolate=false&outputSRS=4326&locationDescriptor=any&setBack=0&brief=false"; + var url = "https://geocoder.api.gov.bc.ca/addresses.json?addressString=address%2Bwith%2Bencoding&maxResults=5&interpolation=adaptive&echo=false&autoComplete=true&exactSpelling=true&fuzzyMatch=false&minScore=0&maxDistance=0&provinceCode=BC&extrapolate=false&outputSRS=4326&locationDescriptor=any&setBack=0&brief=false"; result.Should().NotBeNull(); mockRequestClient.Verify(m => m.GetAsync(url), Times.Once()); result.Should().Be(features); @@ -149,7 +149,7 @@ public async Task GetSiteAddressesAsync_StringAddress_Format_Encoding_Success() var result = await service.GetSiteAddressesAsync("address with encoding", "xml"); // Assert - var url = "https://geocoder.api.gov.bc.ca/addresses.xml?ver=1.2&addressString=address%2Bwith%2Bencoding&maxResults=5&interpolation=adaptive&echo=false&autoComplete=false&minScore=0&maxDistance=0&extrapolate=false&outputSRS=4326&locationDescriptor=any&setBack=0&brief=false"; + var url = "https://geocoder.api.gov.bc.ca/addresses.xml?addressString=address%2Bwith%2Bencoding&maxResults=5&interpolation=adaptive&echo=false&autoComplete=true&exactSpelling=true&fuzzyMatch=false&minScore=0&maxDistance=0&provinceCode=BC&extrapolate=false&outputSRS=4326&locationDescriptor=any&setBack=0&brief=false"; result.Should().NotBeNull(); mockRequestClient.Verify(m => m.GetAsync(url), Times.Once()); result.Should().Be(features); @@ -183,7 +183,7 @@ public async Task GetSiteAddressesAsync_Success() var result = await service.GetSiteAddressesAsync(parameters); // Assert - var url = "https://geocoder.api.gov.bc.ca/addresses.json?ver=1.2&addressString=address%20with%20encoding&maxResults=5&interpolation=adaptive&echo=false&autoComplete=false&minScore=0&maxDistance=0&extrapolate=false&outputSRS=4326&locationDescriptor=any&setBack=0&brief=false"; + var url = "https://geocoder.api.gov.bc.ca/addresses.json?addressString=address%20with%20encoding&maxResults=5&interpolation=adaptive&echo=false&autoComplete=true&exactSpelling=true&fuzzyMatch=false&minScore=0&maxDistance=0&provinceCode=BC&extrapolate=false&outputSRS=4326&locationDescriptor=any&setBack=0&brief=false"; result.Should().NotBeNull(); mockRequestClient.Verify(m => m.GetAsync(url), Times.Once()); result.Should().Be(features); @@ -217,7 +217,7 @@ public async Task GetSiteAddressesAsync_Format_Success() var result = await service.GetSiteAddressesAsync(parameters, "xml"); // Assert - var url = "https://geocoder.api.gov.bc.ca/addresses.xml?ver=1.2&addressString=address%20with%20encoding&maxResults=5&interpolation=adaptive&echo=false&autoComplete=false&minScore=0&maxDistance=0&extrapolate=false&outputSRS=4326&locationDescriptor=any&setBack=0&brief=false"; + var url = "https://geocoder.api.gov.bc.ca/addresses.xml?addressString=address%20with%20encoding&maxResults=5&interpolation=adaptive&echo=false&autoComplete=true&exactSpelling=true&fuzzyMatch=false&minScore=0&maxDistance=0&provinceCode=BC&extrapolate=false&outputSRS=4326&locationDescriptor=any&setBack=0&brief=false"; result.Should().NotBeNull(); mockRequestClient.Verify(m => m.GetAsync(url), Times.Once()); result.Should().Be(features); diff --git a/source/backend/tests/dal/Libraries/Geocoder/ParameterExtensionsTest.cs b/source/backend/tests/dal/Libraries/Geocoder/ParameterExtensionsTest.cs index 43db005e85..6eca50360d 100644 --- a/source/backend/tests/dal/Libraries/Geocoder/ParameterExtensionsTest.cs +++ b/source/backend/tests/dal/Libraries/Geocoder/ParameterExtensionsTest.cs @@ -48,7 +48,6 @@ public void ToQueryStringDictionary_Success(AddressesParameters address) result[nameof(address.MaxDistance).LowercaseFirstCharacter()].Should().Be($"{address.MaxDistance}"); result[nameof(address.Extrapolate).LowercaseFirstCharacter()].Should().Be($"{address.Extrapolate}".ToLower()); if (!string.IsNullOrWhiteSpace(address.Interpolation)) result[nameof(address.Interpolation).LowercaseFirstCharacter()].Should().Be(address.Interpolation); - if (!string.IsNullOrWhiteSpace(address.Ver)) result[nameof(address.Ver).LowercaseFirstCharacter()].Should().Be(address.Ver); if (!string.IsNullOrWhiteSpace(address.AddressString)) result[nameof(address.AddressString).LowercaseFirstCharacter()].Should().Be(address.AddressString); if (!string.IsNullOrWhiteSpace(address.LocationDescriptor)) result[nameof(address.LocationDescriptor).LowercaseFirstCharacter()].Should().Be(address.LocationDescriptor); if (!string.IsNullOrWhiteSpace(address.MatchPrecision)) result[nameof(address.MatchPrecision).LowercaseFirstCharacter()].Should().Be(address.MatchPrecision); diff --git a/source/backend/tests/dal/Repositories/DocumentQueueRepositoryTest.cs b/source/backend/tests/dal/Repositories/DocumentQueueRepositoryTest.cs index 05076834e0..ad503e19e4 100644 --- a/source/backend/tests/dal/Repositories/DocumentQueueRepositoryTest.cs +++ b/source/backend/tests/dal/Repositories/DocumentQueueRepositoryTest.cs @@ -13,6 +13,7 @@ using Pims.Dal.Entities.Models; using System.Security; using Pims.Api.Models.CodeTypes; +using System.Threading.Tasks; namespace Pims.Dal.Test.Repositories { @@ -108,10 +109,10 @@ public void Update_Null() var repository = helper.CreateRepository(user); // Act - Action act = () => repository.Update(null); + Func act = () => repository.Update(null); // Assert - act.Should().Throw(); + act.Should().ThrowAsync(); } #endregion diff --git a/source/backend/tests/scheduler/Services/DocumentQueueServiceTests.cs b/source/backend/tests/scheduler/Services/DocumentQueueServiceTests.cs index 01755e57e8..8ee3e35b6f 100644 --- a/source/backend/tests/scheduler/Services/DocumentQueueServiceTests.cs +++ b/source/backend/tests/scheduler/Services/DocumentQueueServiceTests.cs @@ -81,7 +81,7 @@ public async Task UploadQueuedDocuments_ErrorStatus_ReturnsError() public async Task UploadQueuedDocuments_SingleDocumentError_ReturnsError() { // Arrange - var document = new DocumentQueueModel { Id = 1, DocumentQueueStatusType = new CodeTypeModel() { Id = DocumentQueueStatusTypes.PROCESSING.ToString() } }; + var document = new DocumentQueueModel { Id = 1, DocumentQueueStatusType = new CodeTypeModel() { Id = DocumentQueueStatusTypes.PENDING.ToString() } }; var searchResponse = new ExternalResponse> { Status = ExternalResponseStatus.Success, @@ -106,7 +106,7 @@ public async Task UploadQueuedDocuments_SingleDocumentError_ReturnsError() public async Task UploadQueuedDocuments_SingleDocumentError_ReturnsError_UpdatesQueue() { // Arrange - var document = new DocumentQueueModel { Id = 1, DocumentQueueStatusType = new CodeTypeModel() { Id = DocumentQueueStatusTypes.PROCESSING.ToString() } }; + var document = new DocumentQueueModel { Id = 1, DocumentQueueStatusType = new CodeTypeModel() { Id = DocumentQueueStatusTypes.PENDING.ToString() } }; var searchResponse = new ExternalResponse> { Status = ExternalResponseStatus.Success, @@ -136,7 +136,8 @@ public async Task UploadQueuedDocuments_SingleDocumentError_ReturnsError_Updates public async Task UploadQueuedDocuments_SingleDocumentSuccess_ReturnsSuccess() { // Arrange - var document = new DocumentQueueModel { Id = 1, DocumentQueueStatusType = new CodeTypeModel() { Id = DocumentQueueStatusTypes.PROCESSING.ToString() } }; + var document = new DocumentQueueModel { Id = 1, DocumentQueueStatusType = new CodeTypeModel() { Id = DocumentQueueStatusTypes.PENDING.ToString() } }; + var responseDocument = new DocumentQueueModel { Id = 1, DocumentQueueStatusType = new CodeTypeModel() { Id = DocumentQueueStatusTypes.PROCESSING.ToString() } }; var searchResponse = new ExternalResponse> { Status = ExternalResponseStatus.Success, @@ -146,7 +147,7 @@ public async Task UploadQueuedDocuments_SingleDocumentSuccess_ReturnsSuccess() _documentQueueRepositoryMock.Setup(x => x.UploadQueuedDocument(document)).ReturnsAsync(new ExternalResponse { Status = ExternalResponseStatus.Success, - Payload = document, + Payload = responseDocument, }); // Act @@ -156,12 +157,38 @@ public async Task UploadQueuedDocuments_SingleDocumentSuccess_ReturnsSuccess() result.Status.Should().Be(TaskResponseStatusTypes.SUCCESS); } + [Fact] + public async Task UploadQueuedDocuments_SingleDocumentSkipProcessing() + { + // Arrange + var document = new DocumentQueueModel { Id = 1, DocumentQueueStatusType = new CodeTypeModel() { Id = DocumentQueueStatusTypes.PROCESSING.ToString() } }; + var responseDocument = new DocumentQueueModel { Id = 1, DocumentQueueStatusType = new CodeTypeModel() { Id = DocumentQueueStatusTypes.PROCESSING.ToString() } }; + var searchResponse = new ExternalResponse> + { + Status = ExternalResponseStatus.Success, + Payload = new List { document }, + }; + _documentQueueRepositoryMock.Setup(x => x.SearchQueuedDocumentsAsync(It.IsAny())).ReturnsAsync(searchResponse); + _documentQueueRepositoryMock.Setup(x => x.UploadQueuedDocument(document)).ReturnsAsync(new ExternalResponse + { + Status = ExternalResponseStatus.Success, + Payload = responseDocument, + }); + + // Act + var result = await _service.UploadQueuedDocuments(); + + // Assert + result.Status.Should().Be(TaskResponseStatusTypes.SKIPPED); + } + [Fact] public async Task UploadQueuedDocuments_TwoDocumentsMixedResults_ReturnsPartialSuccess() { // Arrange - var document1 = new DocumentQueueModel { Id = 1, DocumentQueueStatusType = new CodeTypeModel() { Id = DocumentQueueStatusTypes.PROCESSING.ToString() } }; - var document2 = new DocumentQueueModel { Id = 2, DocumentQueueStatusType = new CodeTypeModel() { Id = DocumentQueueStatusTypes.PROCESSING.ToString() } }; + var document1 = new DocumentQueueModel { Id = 1, DocumentQueueStatusType = new CodeTypeModel() { Id = DocumentQueueStatusTypes.PENDING.ToString() } }; + var document2 = new DocumentQueueModel { Id = 2, DocumentQueueStatusType = new CodeTypeModel() { Id = DocumentQueueStatusTypes.PENDING.ToString() } }; + var responseDocument1 = new DocumentQueueModel { Id = 1, DocumentQueueStatusType = new CodeTypeModel() { Id = DocumentQueueStatusTypes.PROCESSING.ToString() } }; var searchResponse = new ExternalResponse> { Status = ExternalResponseStatus.Success, @@ -171,7 +198,7 @@ public async Task UploadQueuedDocuments_TwoDocumentsMixedResults_ReturnsPartialS _documentQueueRepositoryMock.Setup(x => x.UploadQueuedDocument(document1)).ReturnsAsync(new ExternalResponse { Status = ExternalResponseStatus.Success, - Payload = document1, + Payload = responseDocument1, }); _documentQueueRepositoryMock.Setup(x => x.UploadQueuedDocument(document2)).ReturnsAsync(new ExternalResponse { diff --git a/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Down/135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE_Alter_Down.sql b/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Down/135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE_Alter_Down.sql index fe8f1e52e2..5bf40964a3 100644 --- a/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Down/135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE_Alter_Down.sql +++ b/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Down/135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE_Alter_Down.sql @@ -4,6 +4,7 @@ Alter the PIMS_PROP_MGMT_ACTIVITY_TYPE table. Author Date Comment ------------ ----------- ----------------------------------------------------- Doug Filteau 2025-May-15 Initial version. +Doug Filteau 2025-Jun-27 Added UNKNOWN activity type. ----------------------------------------------------------------------------- */ SET XACT_ABORT ON @@ -33,6 +34,24 @@ IF @@ROWCOUNT = 1 GO IF @@ERROR <> 0 SET NOEXEC ON GO + +-- -------------------------------------------------------------- +-- Insert the disabled 'UNKNOWN' activity type. +-- -------------------------------------------------------------- +PRINT N'Insert the disabled "UNKNOWN" activity type.' +GO +DECLARE @CurrCd NVARCHAR(20) +SET @CurrCd = N'UNKNOWN' + +SELECT PROP_MGMT_ACTIVITY_TYPE_CODE +FROM PIMS_PROP_MGMT_ACTIVITY_TYPE +WHERE PROP_MGMT_ACTIVITY_TYPE_CODE = @CurrCd; + +IF @@ROWCOUNT = 0 + INSERT INTO PIMS_PROP_MGMT_ACTIVITY_TYPE (PROP_MGMT_ACTIVITY_TYPE_CODE, DESCRIPTION, IS_DISABLED) + VALUES + (N'UNKNOWN', N'Unknown', 1); +GO -- -------------------------------------------------------------- -- Update the display order. diff --git a/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Down/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE_Alter_Down.sql b/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Down/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE_Alter_Down.sql index dfdf089716..2a270f8a22 100644 --- a/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Down/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE_Alter_Down.sql +++ b/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Down/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE_Alter_Down.sql @@ -4,6 +4,7 @@ Alter the PIMS_PROP_MGMT_ACTIVITY_SUBTYPE table. Author Date Comment ------------ ----------- ----------------------------------------------------- Doug Filteau 2023-May-15 Initial version. +Doug Filteau 2025-Jun-27 Added UNKNOWN activity type. ----------------------------------------------------------------------------- */ SET XACT_ABORT ON @@ -35,6 +36,24 @@ IF @@ROWCOUNT = 1 GO IF @@ERROR <> 0 SET NOEXEC ON GO + +-- -------------------------------------------------------------- +-- Insert the disabled 'UNKNOWN' activity subtype. +-- -------------------------------------------------------------- +PRINT N'Insert the disabled "UNKNOWN" activity subtype.' +GO +DECLARE @CurrCd NVARCHAR(20) +SET @CurrCd = N'UNKNOWN' + +SELECT PROP_MGMT_ACTIVITY_SUBTYPE_CODE +FROM PIMS_PROP_MGMT_ACTIVITY_SUBTYPE +WHERE PROP_MGMT_ACTIVITY_SUBTYPE_CODE = @CurrCd; + +IF @@ROWCOUNT = 0 + INSERT INTO PIMS_PROP_MGMT_ACTIVITY_SUBTYPE (PROP_MGMT_ACTIVITY_TYPE_CODE, PROP_MGMT_ACTIVITY_SUBTYPE_CODE, DESCRIPTION, IS_DISABLED) + VALUES + (N'UNKNOWN', N'UNKNOWN', N'Unknown', 1); +GO -- -------------------------------------------------------------- -- Update the display order. diff --git a/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Down/997_PIMS_PROPERTY_BOUNDARY_RESEARCH_VW_Alter_Down.sql b/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Down/997_PIMS_PROPERTY_BOUNDARY_RESEARCH_VW_Alter_Down.sql new file mode 100644 index 0000000000..ee5e9feb43 --- /dev/null +++ b/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Down/997_PIMS_PROPERTY_BOUNDARY_RESEARCH_VW_Alter_Down.sql @@ -0,0 +1,55 @@ +/* ----------------------------------------------------------------------------- +Alter the PIMS_PROPERTY_BOUNDARY_RESEARCH_VW view. +. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +Author Date Comment +------------ ----------- ----------------------------------------------------- +Doug Filteau 2023-Jun-20 Initial version. +----------------------------------------------------------------------------- */ + +SET XACT_ABORT ON +GO +SET TRANSACTION ISOLATION LEVEL SERIALIZABLE +GO +BEGIN TRANSACTION +GO +IF @@ERROR <> 0 SET NOEXEC ON +GO + +-- Drop view PIMS_PROPERTY_BOUNDARY_RESEARCH_VW +PRINT N'Drop view PIMS_PROPERTY_BOUNDARY_RESEARCH_VW' +GO +DROP VIEW IF EXISTS [dbo].[PIMS_PROPERTY_BOUNDARY_RESEARCH_VW]; +GO +IF @@ERROR <> 0 SET NOEXEC ON +GO + +-- Create view PIMS_PROPERTY_BOUNDARY_RESEARCH_VW +PRINT N'Create view PIMS_PROPERTY_BOUNDARY_RESEARCH_VW' +GO +CREATE VIEW [dbo].[PIMS_PROPERTY_BOUNDARY_RESEARCH_VW] AS +with cteDistinct (PROPERTY_ID) AS + (SELECT DISTINCT prp.PROPERTY_ID + FROM PIMS_PROPERTY prp JOIN + PIMS_PROPERTY_RESEARCH_FILE prf ON prf.PROPERTY_ID = prp.PROPERTY_ID) + +SELECT ct.PROPERTY_ID + , pr.LOCATION +FROM cteDistinct ct JOIN + PIMS_PROPERTY pr ON pr.PROPERTY_ID = ct.PROPERTY_ID +GO +IF @@ERROR <> 0 SET NOEXEC ON +GO + +COMMIT TRANSACTION +GO +IF @@ERROR <> 0 SET NOEXEC ON +GO +DECLARE @Success AS BIT +SET @Success = 1 +SET NOEXEC OFF +IF (@Success = 1) PRINT 'The database update succeeded' +ELSE BEGIN + IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION + PRINT 'The database update failed' +END +GO diff --git a/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Down/master.sql b/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Down/master.sql index 42a7f14a94..ee2a0bcab0 100644 --- a/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Down/master.sql +++ b/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Down/master.sql @@ -1,4 +1,4 @@ --- File generated on 05/27/2025 12:55:30 PM. +-- File generated on 06/27/2025 04:48:27 PM. -- Autogenerated file. Do not manually modify. :On Error Exit @@ -29,6 +29,9 @@ PRINT ' == DB TRANSACTION START ========' PRINT '- Executing PSP_PIMS_S105_00/Alter Down/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE_Alter_Down.sql ' :setvar filepath "PSP_PIMS_S105_00/Alter Down/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE_Alter_Down.sql" :r $(filepath) + PRINT '- Executing PSP_PIMS_S105_00/Alter Down/997_PIMS_PROPERTY_BOUNDARY_RESEARCH_VW_Alter_Down.sql ' + :setvar filepath "PSP_PIMS_S105_00/Alter Down/997_PIMS_PROPERTY_BOUNDARY_RESEARCH_VW_Alter_Down.sql" + :r $(filepath) PRINT '- Executing PSP_PIMS_S105_00/Alter Down/998_PIMS_PROPERTY_ACTIVITY_Load.sql ' :setvar filepath "PSP_PIMS_S105_00/Alter Down/998_PIMS_PROPERTY_ACTIVITY_Load.sql" :r $(filepath) diff --git a/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Up/135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE_Alter_Up.sql b/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Up/135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE_Alter_Up.sql index 157c532464..eb0d7cacb2 100644 --- a/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Up/135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE_Alter_Up.sql +++ b/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Up/135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE_Alter_Up.sql @@ -4,6 +4,7 @@ Alter the PIMS_PROP_MGMT_ACTIVITY_TYPE table. Author Date Comment ------------ ----------- ----------------------------------------------------- Doug Filteau 2025-May-15 Initial version. +Doug Filteau 2025-Jun-27 Added UNKNOWN activity type. ----------------------------------------------------------------------------- */ SET XACT_ABORT ON @@ -36,6 +37,24 @@ ELSE GO IF @@ERROR <> 0 SET NOEXEC ON GO + +-- -------------------------------------------------------------- +-- Insert the disabled 'UNKNOWN' activity type. +-- -------------------------------------------------------------- +PRINT N'Insert the disabled "UNKNOWN" activity type.' +GO +DECLARE @CurrCd NVARCHAR(20) +SET @CurrCd = N'UNKNOWN' + +SELECT PROP_MGMT_ACTIVITY_TYPE_CODE +FROM PIMS_PROP_MGMT_ACTIVITY_TYPE +WHERE PROP_MGMT_ACTIVITY_TYPE_CODE = @CurrCd; + +IF @@ROWCOUNT = 0 + INSERT INTO PIMS_PROP_MGMT_ACTIVITY_TYPE (PROP_MGMT_ACTIVITY_TYPE_CODE, DESCRIPTION, IS_DISABLED) + VALUES + (N'UNKNOWN', N'Unknown', 1); +GO -- -------------------------------------------------------------- -- Update the display order. diff --git a/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Up/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE_Alter_Up.sql b/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Up/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE_Alter_Up.sql index 5c496d0dbe..a01aae01fd 100644 --- a/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Up/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE_Alter_Up.sql +++ b/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Up/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE_Alter_Up.sql @@ -4,6 +4,7 @@ Alter the PIMS_PROP_MGMT_ACTIVITY_SUBTYPE table. Author Date Comment ------------ ----------- ----------------------------------------------------- Doug Filteau 2023-May-15 Initial version. +Doug Filteau 2025-Jun-27 Added UNKNOWN activity type. ----------------------------------------------------------------------------- */ SET XACT_ABORT ON @@ -38,6 +39,24 @@ ELSE GO IF @@ERROR <> 0 SET NOEXEC ON GO + +-- -------------------------------------------------------------- +-- Insert the disabled 'UNKNOWN' activity subtype. +-- -------------------------------------------------------------- +PRINT N'Insert the disabled "UNKNOWN" activity subtype.' +GO +DECLARE @CurrCd NVARCHAR(20) +SET @CurrCd = N'UNKNOWN' + +SELECT PROP_MGMT_ACTIVITY_SUBTYPE_CODE +FROM PIMS_PROP_MGMT_ACTIVITY_SUBTYPE +WHERE PROP_MGMT_ACTIVITY_SUBTYPE_CODE = @CurrCd; + +IF @@ROWCOUNT = 0 + INSERT INTO PIMS_PROP_MGMT_ACTIVITY_SUBTYPE (PROP_MGMT_ACTIVITY_TYPE_CODE, PROP_MGMT_ACTIVITY_SUBTYPE_CODE, DESCRIPTION, IS_DISABLED) + VALUES + (N'UNKNOWN', N'UNKNOWN', N'Unknown', 1); +GO -- -------------------------------------------------------------- -- Update the display order. diff --git a/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Up/997_PIMS_PROPERTY_BOUNDARY_RESEARCH_VW_Alter_Up.sql b/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Up/997_PIMS_PROPERTY_BOUNDARY_RESEARCH_VW_Alter_Up.sql new file mode 100644 index 0000000000..4ea7e12f43 --- /dev/null +++ b/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Up/997_PIMS_PROPERTY_BOUNDARY_RESEARCH_VW_Alter_Up.sql @@ -0,0 +1,55 @@ +/* ----------------------------------------------------------------------------- +Alter the PIMS_PROPERTY_BOUNDARY_RESEARCH_VW view. +. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +Author Date Comment +------------ ----------- ----------------------------------------------------- +Doug Filteau 2023-Jun-20 Initial version. +----------------------------------------------------------------------------- */ + +SET XACT_ABORT ON +GO +SET TRANSACTION ISOLATION LEVEL SERIALIZABLE +GO +BEGIN TRANSACTION +GO +IF @@ERROR <> 0 SET NOEXEC ON +GO + +-- Drop view PIMS_PROPERTY_BOUNDARY_RESEARCH_VW +PRINT N'Drop view PIMS_PROPERTY_BOUNDARY_RESEARCH_VW' +GO +DROP VIEW IF EXISTS [dbo].[PIMS_PROPERTY_BOUNDARY_RESEARCH_VW]; +GO +IF @@ERROR <> 0 SET NOEXEC ON +GO + +-- Create view PIMS_PROPERTY_BOUNDARY_RESEARCH_VW +PRINT N'Create view PIMS_PROPERTY_BOUNDARY_RESEARCH_VW' +GO +CREATE VIEW [dbo].[PIMS_PROPERTY_BOUNDARY_RESEARCH_VW] AS +with cteDistinct (PROPERTY_ID) AS + (SELECT DISTINCT prp.PROPERTY_ID + FROM PIMS_PROPERTY prp JOIN + PIMS_PROPERTY_RESEARCH_FILE prf ON prf.PROPERTY_ID = prp.PROPERTY_ID) + +SELECT ct.PROPERTY_ID + , pr.BOUNDARY +FROM cteDistinct ct JOIN + PIMS_PROPERTY pr ON pr.PROPERTY_ID = ct.PROPERTY_ID +GO +IF @@ERROR <> 0 SET NOEXEC ON +GO + +COMMIT TRANSACTION +GO +IF @@ERROR <> 0 SET NOEXEC ON +GO +DECLARE @Success AS BIT +SET @Success = 1 +SET NOEXEC OFF +IF (@Success = 1) PRINT 'The database update succeeded' +ELSE BEGIN + IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION + PRINT 'The database update failed' +END +GO diff --git a/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Up/master.sql b/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Up/master.sql index 7605f21b84..9cd87df35d 100644 --- a/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Up/master.sql +++ b/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Alter Up/master.sql @@ -1,4 +1,4 @@ --- File generated on 05/27/2025 12:55:30 PM. +-- File generated on 06/27/2025 04:48:27 PM. -- Autogenerated file. Do not manually modify. :On Error Exit @@ -32,6 +32,9 @@ PRINT ' == DB TRANSACTION START ========' PRINT '- Executing PSP_PIMS_S105_00/Alter Up/171_DML_PIMS_TENURE_CLEANUP_TYPE.sql ' :setvar filepath "PSP_PIMS_S105_00/Alter Up/171_DML_PIMS_TENURE_CLEANUP_TYPE.sql" :r $(filepath) + PRINT '- Executing PSP_PIMS_S105_00/Alter Up/997_PIMS_PROPERTY_BOUNDARY_RESEARCH_VW_Alter_Up.sql ' + :setvar filepath "PSP_PIMS_S105_00/Alter Up/997_PIMS_PROPERTY_BOUNDARY_RESEARCH_VW_Alter_Up.sql" + :r $(filepath) PRINT '- Executing PSP_PIMS_S105_00/Alter Up/998_PIMS_PROPERTY_ACTIVITY_Load.sql ' :setvar filepath "PSP_PIMS_S105_00/Alter Up/998_PIMS_PROPERTY_ACTIVITY_Load.sql" :r $(filepath) diff --git a/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Build/002_PSP_PIMS_Build.sql b/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Build/002_PSP_PIMS_Build.sql index 1730f8c505..c30e4e1aa6 100644 --- a/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Build/002_PSP_PIMS_Build.sql +++ b/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Build/002_PSP_PIMS_Build.sql @@ -23067,7 +23067,7 @@ with cteDistinct (PROPERTY_ID) AS PIMS_PROPERTY_RESEARCH_FILE prf ON prf.PROPERTY_ID = prp.PROPERTY_ID) SELECT ct.PROPERTY_ID - , pr.LOCATION + , pr.BOUNDARY FROM cteDistinct ct JOIN PIMS_PROPERTY pr ON pr.PROPERTY_ID = ct.PROPERTY_ID GO diff --git a/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Build/135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE.sql b/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Build/135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE.sql index fb3015f7af..2343661867 100644 --- a/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Build/135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE.sql +++ b/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Build/135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE.sql @@ -6,6 +6,7 @@ Author Date Comment Doug Filteau 2023-Sep-11 Initial version. Doug Filteau 2024-Feb-26 Added UTILITYBILL and TAXESLEVIES. Doug Filteau 2025-May-07 Added CONSULTATION and TRAILMTC. +Doug Filteau 2025-Jun-27 Added UNKNOWN activity type. ----------------------------------------------------------------------------- */ DELETE FROM PIMS_PROP_MGMT_ACTIVITY_TYPE @@ -28,7 +29,15 @@ VALUES (N'TRAILMTC', N'Trail Maintenance'), (N'CORRESPOND', N'Correspondence'); GO - + +-- -------------------------------------------------------------- +-- Insert the disabled 'UNKNOWN' activity type. +-- -------------------------------------------------------------- +INSERT INTO PIMS_PROP_MGMT_ACTIVITY_TYPE (PROP_MGMT_ACTIVITY_TYPE_CODE, DESCRIPTION, IS_DISABLED) +VALUES + (N'UNKNOWN', N'Unknown', 1); +GO + -- -------------------------------------------------------------- -- Update the display order. -- -------------------------------------------------------------- diff --git a/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Build/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE.sql b/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Build/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE.sql index e93592afc2..d87b13a1ca 100644 --- a/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Build/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE.sql +++ b/source/database/mssql/scripts/dbscripts/PSP_PIMS_LATEST/Build/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE.sql @@ -9,6 +9,7 @@ Doug Filteau 2024-Jul-03 Add/enable PROPADMIN and remove the leading space from WATERANDSEWER. Doug Filteau 2024-Jul-11 Added BYLAWINFRAC. Doug Filteau 2025-May-07 Added CONSULTATION and TRAILMTC. +Doug Filteau 2025-Jun-27 Added UNKNOWN activity subtype. ----------------------------------------------------------------------------- */ DELETE FROM PIMS_PROP_MGMT_ACTIVITY_SUBTYPE @@ -96,6 +97,14 @@ VALUES (N'CORRESPOND', N'CORRESPOND', N'Correspondence'); GO + +-- -------------------------------------------------------------- +-- Insert the disabled 'UNKNOWN' activity subtype. +-- -------------------------------------------------------------- +INSERT INTO PIMS_PROP_MGMT_ACTIVITY_SUBTYPE (PROP_MGMT_ACTIVITY_TYPE_CODE, PROP_MGMT_ACTIVITY_SUBTYPE_CODE, DESCRIPTION, IS_DISABLED) +VALUES + (N'UNKNOWN', N'UNKNOWN', N'Unknown', 1); +GO -- -------------------------------------------------------------- -- Update the display order. diff --git a/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Down/135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE_Alter_Down.sql b/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Down/135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE_Alter_Down.sql index fe8f1e52e2..5bf40964a3 100644 --- a/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Down/135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE_Alter_Down.sql +++ b/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Down/135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE_Alter_Down.sql @@ -4,6 +4,7 @@ Alter the PIMS_PROP_MGMT_ACTIVITY_TYPE table. Author Date Comment ------------ ----------- ----------------------------------------------------- Doug Filteau 2025-May-15 Initial version. +Doug Filteau 2025-Jun-27 Added UNKNOWN activity type. ----------------------------------------------------------------------------- */ SET XACT_ABORT ON @@ -33,6 +34,24 @@ IF @@ROWCOUNT = 1 GO IF @@ERROR <> 0 SET NOEXEC ON GO + +-- -------------------------------------------------------------- +-- Insert the disabled 'UNKNOWN' activity type. +-- -------------------------------------------------------------- +PRINT N'Insert the disabled "UNKNOWN" activity type.' +GO +DECLARE @CurrCd NVARCHAR(20) +SET @CurrCd = N'UNKNOWN' + +SELECT PROP_MGMT_ACTIVITY_TYPE_CODE +FROM PIMS_PROP_MGMT_ACTIVITY_TYPE +WHERE PROP_MGMT_ACTIVITY_TYPE_CODE = @CurrCd; + +IF @@ROWCOUNT = 0 + INSERT INTO PIMS_PROP_MGMT_ACTIVITY_TYPE (PROP_MGMT_ACTIVITY_TYPE_CODE, DESCRIPTION, IS_DISABLED) + VALUES + (N'UNKNOWN', N'Unknown', 1); +GO -- -------------------------------------------------------------- -- Update the display order. diff --git a/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Down/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE_Alter_Down.sql b/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Down/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE_Alter_Down.sql index dfdf089716..2a270f8a22 100644 --- a/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Down/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE_Alter_Down.sql +++ b/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Down/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE_Alter_Down.sql @@ -4,6 +4,7 @@ Alter the PIMS_PROP_MGMT_ACTIVITY_SUBTYPE table. Author Date Comment ------------ ----------- ----------------------------------------------------- Doug Filteau 2023-May-15 Initial version. +Doug Filteau 2025-Jun-27 Added UNKNOWN activity type. ----------------------------------------------------------------------------- */ SET XACT_ABORT ON @@ -35,6 +36,24 @@ IF @@ROWCOUNT = 1 GO IF @@ERROR <> 0 SET NOEXEC ON GO + +-- -------------------------------------------------------------- +-- Insert the disabled 'UNKNOWN' activity subtype. +-- -------------------------------------------------------------- +PRINT N'Insert the disabled "UNKNOWN" activity subtype.' +GO +DECLARE @CurrCd NVARCHAR(20) +SET @CurrCd = N'UNKNOWN' + +SELECT PROP_MGMT_ACTIVITY_SUBTYPE_CODE +FROM PIMS_PROP_MGMT_ACTIVITY_SUBTYPE +WHERE PROP_MGMT_ACTIVITY_SUBTYPE_CODE = @CurrCd; + +IF @@ROWCOUNT = 0 + INSERT INTO PIMS_PROP_MGMT_ACTIVITY_SUBTYPE (PROP_MGMT_ACTIVITY_TYPE_CODE, PROP_MGMT_ACTIVITY_SUBTYPE_CODE, DESCRIPTION, IS_DISABLED) + VALUES + (N'UNKNOWN', N'UNKNOWN', N'Unknown', 1); +GO -- -------------------------------------------------------------- -- Update the display order. diff --git a/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Down/997_PIMS_PROPERTY_BOUNDARY_RESEARCH_VW_Alter_Down.sql b/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Down/997_PIMS_PROPERTY_BOUNDARY_RESEARCH_VW_Alter_Down.sql new file mode 100644 index 0000000000..ee5e9feb43 --- /dev/null +++ b/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Down/997_PIMS_PROPERTY_BOUNDARY_RESEARCH_VW_Alter_Down.sql @@ -0,0 +1,55 @@ +/* ----------------------------------------------------------------------------- +Alter the PIMS_PROPERTY_BOUNDARY_RESEARCH_VW view. +. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +Author Date Comment +------------ ----------- ----------------------------------------------------- +Doug Filteau 2023-Jun-20 Initial version. +----------------------------------------------------------------------------- */ + +SET XACT_ABORT ON +GO +SET TRANSACTION ISOLATION LEVEL SERIALIZABLE +GO +BEGIN TRANSACTION +GO +IF @@ERROR <> 0 SET NOEXEC ON +GO + +-- Drop view PIMS_PROPERTY_BOUNDARY_RESEARCH_VW +PRINT N'Drop view PIMS_PROPERTY_BOUNDARY_RESEARCH_VW' +GO +DROP VIEW IF EXISTS [dbo].[PIMS_PROPERTY_BOUNDARY_RESEARCH_VW]; +GO +IF @@ERROR <> 0 SET NOEXEC ON +GO + +-- Create view PIMS_PROPERTY_BOUNDARY_RESEARCH_VW +PRINT N'Create view PIMS_PROPERTY_BOUNDARY_RESEARCH_VW' +GO +CREATE VIEW [dbo].[PIMS_PROPERTY_BOUNDARY_RESEARCH_VW] AS +with cteDistinct (PROPERTY_ID) AS + (SELECT DISTINCT prp.PROPERTY_ID + FROM PIMS_PROPERTY prp JOIN + PIMS_PROPERTY_RESEARCH_FILE prf ON prf.PROPERTY_ID = prp.PROPERTY_ID) + +SELECT ct.PROPERTY_ID + , pr.LOCATION +FROM cteDistinct ct JOIN + PIMS_PROPERTY pr ON pr.PROPERTY_ID = ct.PROPERTY_ID +GO +IF @@ERROR <> 0 SET NOEXEC ON +GO + +COMMIT TRANSACTION +GO +IF @@ERROR <> 0 SET NOEXEC ON +GO +DECLARE @Success AS BIT +SET @Success = 1 +SET NOEXEC OFF +IF (@Success = 1) PRINT 'The database update succeeded' +ELSE BEGIN + IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION + PRINT 'The database update failed' +END +GO diff --git a/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Down/master.sql b/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Down/master.sql index 42a7f14a94..ee2a0bcab0 100644 --- a/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Down/master.sql +++ b/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Down/master.sql @@ -1,4 +1,4 @@ --- File generated on 05/27/2025 12:55:30 PM. +-- File generated on 06/27/2025 04:48:27 PM. -- Autogenerated file. Do not manually modify. :On Error Exit @@ -29,6 +29,9 @@ PRINT ' == DB TRANSACTION START ========' PRINT '- Executing PSP_PIMS_S105_00/Alter Down/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE_Alter_Down.sql ' :setvar filepath "PSP_PIMS_S105_00/Alter Down/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE_Alter_Down.sql" :r $(filepath) + PRINT '- Executing PSP_PIMS_S105_00/Alter Down/997_PIMS_PROPERTY_BOUNDARY_RESEARCH_VW_Alter_Down.sql ' + :setvar filepath "PSP_PIMS_S105_00/Alter Down/997_PIMS_PROPERTY_BOUNDARY_RESEARCH_VW_Alter_Down.sql" + :r $(filepath) PRINT '- Executing PSP_PIMS_S105_00/Alter Down/998_PIMS_PROPERTY_ACTIVITY_Load.sql ' :setvar filepath "PSP_PIMS_S105_00/Alter Down/998_PIMS_PROPERTY_ACTIVITY_Load.sql" :r $(filepath) diff --git a/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Up/135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE_Alter_Up.sql b/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Up/135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE_Alter_Up.sql index 157c532464..eb0d7cacb2 100644 --- a/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Up/135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE_Alter_Up.sql +++ b/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Up/135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE_Alter_Up.sql @@ -4,6 +4,7 @@ Alter the PIMS_PROP_MGMT_ACTIVITY_TYPE table. Author Date Comment ------------ ----------- ----------------------------------------------------- Doug Filteau 2025-May-15 Initial version. +Doug Filteau 2025-Jun-27 Added UNKNOWN activity type. ----------------------------------------------------------------------------- */ SET XACT_ABORT ON @@ -36,6 +37,24 @@ ELSE GO IF @@ERROR <> 0 SET NOEXEC ON GO + +-- -------------------------------------------------------------- +-- Insert the disabled 'UNKNOWN' activity type. +-- -------------------------------------------------------------- +PRINT N'Insert the disabled "UNKNOWN" activity type.' +GO +DECLARE @CurrCd NVARCHAR(20) +SET @CurrCd = N'UNKNOWN' + +SELECT PROP_MGMT_ACTIVITY_TYPE_CODE +FROM PIMS_PROP_MGMT_ACTIVITY_TYPE +WHERE PROP_MGMT_ACTIVITY_TYPE_CODE = @CurrCd; + +IF @@ROWCOUNT = 0 + INSERT INTO PIMS_PROP_MGMT_ACTIVITY_TYPE (PROP_MGMT_ACTIVITY_TYPE_CODE, DESCRIPTION, IS_DISABLED) + VALUES + (N'UNKNOWN', N'Unknown', 1); +GO -- -------------------------------------------------------------- -- Update the display order. diff --git a/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Up/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE_Alter_Up.sql b/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Up/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE_Alter_Up.sql index 5c496d0dbe..a01aae01fd 100644 --- a/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Up/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE_Alter_Up.sql +++ b/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Up/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE_Alter_Up.sql @@ -4,6 +4,7 @@ Alter the PIMS_PROP_MGMT_ACTIVITY_SUBTYPE table. Author Date Comment ------------ ----------- ----------------------------------------------------- Doug Filteau 2023-May-15 Initial version. +Doug Filteau 2025-Jun-27 Added UNKNOWN activity type. ----------------------------------------------------------------------------- */ SET XACT_ABORT ON @@ -38,6 +39,24 @@ ELSE GO IF @@ERROR <> 0 SET NOEXEC ON GO + +-- -------------------------------------------------------------- +-- Insert the disabled 'UNKNOWN' activity subtype. +-- -------------------------------------------------------------- +PRINT N'Insert the disabled "UNKNOWN" activity subtype.' +GO +DECLARE @CurrCd NVARCHAR(20) +SET @CurrCd = N'UNKNOWN' + +SELECT PROP_MGMT_ACTIVITY_SUBTYPE_CODE +FROM PIMS_PROP_MGMT_ACTIVITY_SUBTYPE +WHERE PROP_MGMT_ACTIVITY_SUBTYPE_CODE = @CurrCd; + +IF @@ROWCOUNT = 0 + INSERT INTO PIMS_PROP_MGMT_ACTIVITY_SUBTYPE (PROP_MGMT_ACTIVITY_TYPE_CODE, PROP_MGMT_ACTIVITY_SUBTYPE_CODE, DESCRIPTION, IS_DISABLED) + VALUES + (N'UNKNOWN', N'UNKNOWN', N'Unknown', 1); +GO -- -------------------------------------------------------------- -- Update the display order. diff --git a/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Up/997_PIMS_PROPERTY_BOUNDARY_RESEARCH_VW_Alter_Up.sql b/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Up/997_PIMS_PROPERTY_BOUNDARY_RESEARCH_VW_Alter_Up.sql new file mode 100644 index 0000000000..4ea7e12f43 --- /dev/null +++ b/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Up/997_PIMS_PROPERTY_BOUNDARY_RESEARCH_VW_Alter_Up.sql @@ -0,0 +1,55 @@ +/* ----------------------------------------------------------------------------- +Alter the PIMS_PROPERTY_BOUNDARY_RESEARCH_VW view. +. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . +Author Date Comment +------------ ----------- ----------------------------------------------------- +Doug Filteau 2023-Jun-20 Initial version. +----------------------------------------------------------------------------- */ + +SET XACT_ABORT ON +GO +SET TRANSACTION ISOLATION LEVEL SERIALIZABLE +GO +BEGIN TRANSACTION +GO +IF @@ERROR <> 0 SET NOEXEC ON +GO + +-- Drop view PIMS_PROPERTY_BOUNDARY_RESEARCH_VW +PRINT N'Drop view PIMS_PROPERTY_BOUNDARY_RESEARCH_VW' +GO +DROP VIEW IF EXISTS [dbo].[PIMS_PROPERTY_BOUNDARY_RESEARCH_VW]; +GO +IF @@ERROR <> 0 SET NOEXEC ON +GO + +-- Create view PIMS_PROPERTY_BOUNDARY_RESEARCH_VW +PRINT N'Create view PIMS_PROPERTY_BOUNDARY_RESEARCH_VW' +GO +CREATE VIEW [dbo].[PIMS_PROPERTY_BOUNDARY_RESEARCH_VW] AS +with cteDistinct (PROPERTY_ID) AS + (SELECT DISTINCT prp.PROPERTY_ID + FROM PIMS_PROPERTY prp JOIN + PIMS_PROPERTY_RESEARCH_FILE prf ON prf.PROPERTY_ID = prp.PROPERTY_ID) + +SELECT ct.PROPERTY_ID + , pr.BOUNDARY +FROM cteDistinct ct JOIN + PIMS_PROPERTY pr ON pr.PROPERTY_ID = ct.PROPERTY_ID +GO +IF @@ERROR <> 0 SET NOEXEC ON +GO + +COMMIT TRANSACTION +GO +IF @@ERROR <> 0 SET NOEXEC ON +GO +DECLARE @Success AS BIT +SET @Success = 1 +SET NOEXEC OFF +IF (@Success = 1) PRINT 'The database update succeeded' +ELSE BEGIN + IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION + PRINT 'The database update failed' +END +GO diff --git a/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Up/master.sql b/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Up/master.sql index 7605f21b84..9cd87df35d 100644 --- a/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Up/master.sql +++ b/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Alter Up/master.sql @@ -1,4 +1,4 @@ --- File generated on 05/27/2025 12:55:30 PM. +-- File generated on 06/27/2025 04:48:27 PM. -- Autogenerated file. Do not manually modify. :On Error Exit @@ -32,6 +32,9 @@ PRINT ' == DB TRANSACTION START ========' PRINT '- Executing PSP_PIMS_S105_00/Alter Up/171_DML_PIMS_TENURE_CLEANUP_TYPE.sql ' :setvar filepath "PSP_PIMS_S105_00/Alter Up/171_DML_PIMS_TENURE_CLEANUP_TYPE.sql" :r $(filepath) + PRINT '- Executing PSP_PIMS_S105_00/Alter Up/997_PIMS_PROPERTY_BOUNDARY_RESEARCH_VW_Alter_Up.sql ' + :setvar filepath "PSP_PIMS_S105_00/Alter Up/997_PIMS_PROPERTY_BOUNDARY_RESEARCH_VW_Alter_Up.sql" + :r $(filepath) PRINT '- Executing PSP_PIMS_S105_00/Alter Up/998_PIMS_PROPERTY_ACTIVITY_Load.sql ' :setvar filepath "PSP_PIMS_S105_00/Alter Up/998_PIMS_PROPERTY_ACTIVITY_Load.sql" :r $(filepath) diff --git a/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Build/002_PSP_PIMS_Build.sql b/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Build/002_PSP_PIMS_Build.sql index 1730f8c505..c30e4e1aa6 100644 --- a/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Build/002_PSP_PIMS_Build.sql +++ b/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Build/002_PSP_PIMS_Build.sql @@ -23067,7 +23067,7 @@ with cteDistinct (PROPERTY_ID) AS PIMS_PROPERTY_RESEARCH_FILE prf ON prf.PROPERTY_ID = prp.PROPERTY_ID) SELECT ct.PROPERTY_ID - , pr.LOCATION + , pr.BOUNDARY FROM cteDistinct ct JOIN PIMS_PROPERTY pr ON pr.PROPERTY_ID = ct.PROPERTY_ID GO diff --git a/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Build/135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE.sql b/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Build/135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE.sql index fb3015f7af..2343661867 100644 --- a/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Build/135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE.sql +++ b/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Build/135_DML_PIMS_PROP_MGMT_ACTIVITY_TYPE.sql @@ -6,6 +6,7 @@ Author Date Comment Doug Filteau 2023-Sep-11 Initial version. Doug Filteau 2024-Feb-26 Added UTILITYBILL and TAXESLEVIES. Doug Filteau 2025-May-07 Added CONSULTATION and TRAILMTC. +Doug Filteau 2025-Jun-27 Added UNKNOWN activity type. ----------------------------------------------------------------------------- */ DELETE FROM PIMS_PROP_MGMT_ACTIVITY_TYPE @@ -28,7 +29,15 @@ VALUES (N'TRAILMTC', N'Trail Maintenance'), (N'CORRESPOND', N'Correspondence'); GO - + +-- -------------------------------------------------------------- +-- Insert the disabled 'UNKNOWN' activity type. +-- -------------------------------------------------------------- +INSERT INTO PIMS_PROP_MGMT_ACTIVITY_TYPE (PROP_MGMT_ACTIVITY_TYPE_CODE, DESCRIPTION, IS_DISABLED) +VALUES + (N'UNKNOWN', N'Unknown', 1); +GO + -- -------------------------------------------------------------- -- Update the display order. -- -------------------------------------------------------------- diff --git a/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Build/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE.sql b/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Build/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE.sql index e93592afc2..d87b13a1ca 100644 --- a/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Build/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE.sql +++ b/source/database/mssql/scripts/dbscripts/PSP_PIMS_S105_00/Build/136_DML_PIMS_PROP_MGMT_ACTIVITY_SUBTYPE.sql @@ -9,6 +9,7 @@ Doug Filteau 2024-Jul-03 Add/enable PROPADMIN and remove the leading space from WATERANDSEWER. Doug Filteau 2024-Jul-11 Added BYLAWINFRAC. Doug Filteau 2025-May-07 Added CONSULTATION and TRAILMTC. +Doug Filteau 2025-Jun-27 Added UNKNOWN activity subtype. ----------------------------------------------------------------------------- */ DELETE FROM PIMS_PROP_MGMT_ACTIVITY_SUBTYPE @@ -96,6 +97,14 @@ VALUES (N'CORRESPOND', N'CORRESPOND', N'Correspondence'); GO + +-- -------------------------------------------------------------- +-- Insert the disabled 'UNKNOWN' activity subtype. +-- -------------------------------------------------------------- +INSERT INTO PIMS_PROP_MGMT_ACTIVITY_SUBTYPE (PROP_MGMT_ACTIVITY_TYPE_CODE, PROP_MGMT_ACTIVITY_SUBTYPE_CODE, DESCRIPTION, IS_DISABLED) +VALUES + (N'UNKNOWN', N'UNKNOWN', N'Unknown', 1); +GO -- -------------------------------------------------------------- -- Update the display order. diff --git a/source/frontend/package.json b/source/frontend/package.json index 5e6c4e8a82..8ed9a29b0f 100644 --- a/source/frontend/package.json +++ b/source/frontend/package.json @@ -1,6 +1,6 @@ { "name": "frontend", - "version": "5.11.0-106.2", + "version": "5.11.0-106.25", "private": true, "dependencies": { "@bcgov/bc-sans": "1.0.1", diff --git a/source/frontend/src/assets/images/inactive-property.svg b/source/frontend/src/assets/images/inactive-property.svg new file mode 100644 index 0000000000..ab5b72818c --- /dev/null +++ b/source/frontend/src/assets/images/inactive-property.svg @@ -0,0 +1,15 @@ + + + + + + + + + + \ No newline at end of file diff --git a/source/frontend/src/components/common/form/ContactInput/ContactInputContainer.tsx b/source/frontend/src/components/common/form/ContactInput/ContactInputContainer.tsx index 8b817f357e..a3b4cbf516 100644 --- a/source/frontend/src/components/common/form/ContactInput/ContactInputContainer.tsx +++ b/source/frontend/src/components/common/form/ContactInput/ContactInputContainer.tsx @@ -13,6 +13,7 @@ export type IContactInputContainerProps = { displayErrorAsTooltip?: boolean; onContactSelected?: (contact: IContactSearchResult) => void; placeholder?: string; + canEditDetails?: boolean; }; export const ContactInputContainer: React.FC< @@ -29,6 +30,7 @@ export const ContactInputContainer: React.FC< displayErrorAsTooltip = true, onContactSelected, placeholder, + canEditDetails, }) => { const [showContactManager, setShowContactManager] = useState(false); const [selectedContacts, setSelectedContacts] = useState([]); @@ -43,6 +45,8 @@ export const ContactInputContainer: React.FC< } }; + const editEnabled = canEditDetails ?? true; + return ( ); }; diff --git a/source/frontend/src/components/common/form/ContactInput/ContactInputView.test.tsx b/source/frontend/src/components/common/form/ContactInput/ContactInputView.test.tsx index b803f53799..3c1acbbe58 100644 --- a/source/frontend/src/components/common/form/ContactInput/ContactInputView.test.tsx +++ b/source/frontend/src/components/common/form/ContactInput/ContactInputView.test.tsx @@ -54,6 +54,7 @@ describe('ContactInputView component', () => { field: 'test', setShowContactManager: setShowContactManager, onClear: clear, + canEditDetails: true, }); expect(asFragment()).toMatchSnapshot(); }); @@ -64,6 +65,7 @@ describe('ContactInputView component', () => { field: 'test', setShowContactManager: setShowContactManager, onClear: clear, + canEditDetails: true, }); expect(getByText('Select from contacts')).toBeInTheDocument(); }); @@ -75,6 +77,7 @@ describe('ContactInputView component', () => { setShowContactManager: setShowContactManager, onClear: clear, initialValues: { test: { firstName: 'blah', surname: 'blah2', personId: 1 } }, + canEditDetails: true, }); expect(getByText('blah blah2')).toBeInTheDocument(); }); @@ -86,6 +89,7 @@ describe('ContactInputView component', () => { setShowContactManager: setShowContactManager, onClear: clear, initialValues: { test: { organizationName: 'blah org', organizationId: 1 } }, + canEditDetails: true, }); expect(getByText('blah org')).toBeInTheDocument(); }); @@ -96,6 +100,7 @@ describe('ContactInputView component', () => { field: 'test', setShowContactManager: setShowContactManager, onClear: clear, + canEditDetails: true, }); const icon = getByTitle('Select Contact'); await act(async () => userEvent.click(icon)); @@ -108,6 +113,7 @@ describe('ContactInputView component', () => { field: 'test', setShowContactManager: setShowContactManager, onClear: clear, + canEditDetails: true, }); const icon = queryByTitle('remove'); await act(async () => userEvent.click(icon)); @@ -120,6 +126,7 @@ describe('ContactInputView component', () => { field: 'invalid', setShowContactManager: setShowContactManager, onClear: clear, + canEditDetails: true, }); const icon = queryByTitle('remove'); expect(icon).not.toBeInTheDocument(); diff --git a/source/frontend/src/components/common/form/ContactInput/ContactInputView.tsx b/source/frontend/src/components/common/form/ContactInput/ContactInputView.tsx index f7624d6beb..7aee7e6f29 100644 --- a/source/frontend/src/components/common/form/ContactInput/ContactInputView.tsx +++ b/source/frontend/src/components/common/form/ContactInput/ContactInputView.tsx @@ -29,6 +29,7 @@ export type OptionalAttributes = { required?: boolean; displayErrorTooltips?: boolean; placeholder?: string; + canEditDetails: boolean; }; export type IContactInputViewProps = FormControlProps & OptionalAttributes & RequiredAttributes; @@ -41,6 +42,7 @@ const ContactInputView: React.FunctionComponent = ({ setShowContactManager, contactManagerProps, placeholder, + canEditDetails, }) => { const { errors, touched, values } = useFormikContext(); const error = getIn(errors, field); @@ -72,7 +74,7 @@ const ContactInputView: React.FunctionComponent = ({ }} > {text} - {isValidString(contactInfo?.id) && ( + {isValidString(contactInfo?.id) && canEditDetails && ( { onClear(); @@ -88,6 +90,7 @@ const ContactInputView: React.FunctionComponent = ({ placeholder="Select from Contacts" className="d-none" defaultValue="" + disabled={!canEditDetails} > @@ -98,6 +101,7 @@ const ContactInputView: React.FunctionComponent = ({ onClick={() => { setShowContactManager(true); }} + disabled={!canEditDetails} > diff --git a/source/frontend/src/components/common/form/PrimaryContactSelector/PrimaryContactSelector.tsx b/source/frontend/src/components/common/form/PrimaryContactSelector/PrimaryContactSelector.tsx index fdfbbfbbda..520a6ccb17 100644 --- a/source/frontend/src/components/common/form/PrimaryContactSelector/PrimaryContactSelector.tsx +++ b/source/frontend/src/components/common/form/PrimaryContactSelector/PrimaryContactSelector.tsx @@ -11,11 +11,13 @@ import { Select, SelectOption } from '../Select'; export interface IPrimaryContactSelectorProps { field: string; contactInfo?: IContactSearchResult; + canEditDetails?: boolean; } export const PrimaryContactSelector: React.FC = ({ field, contactInfo, + canEditDetails, }) => { const { setFieldValue } = useFormikContext(); const { @@ -47,9 +49,16 @@ export const PrimaryContactSelector: React.FC = ({ }; }) ?? []; + const editEnabled = canEditDetails ?? true; + if (contactInfo?.organizationId && !contactInfo?.personId) { return primaryContacts.length > 1 ? ( - ) : primaryContacts.length > 0 ? ( {primaryContacts[0].label} ) : ( diff --git a/source/frontend/src/components/common/form/ProjectSelector/ProjectSelector.tsx b/source/frontend/src/components/common/form/ProjectSelector/ProjectSelector.tsx index 9fadd4c9d4..60009eff21 100644 --- a/source/frontend/src/components/common/form/ProjectSelector/ProjectSelector.tsx +++ b/source/frontend/src/components/common/form/ProjectSelector/ProjectSelector.tsx @@ -7,6 +7,8 @@ import { IAutocompletePrediction } from '@/interfaces'; export interface IProjectSelectorProps { /** The formik field name */ field: string; + + disabled?: boolean; /* Called whenever the component selection changes. Receives an array of the selected options. */ onChange?: (selected: IAutocompletePrediction[]) => void; } @@ -24,6 +26,7 @@ export const ProjectSelector: React.FC = props => { options={matchedProjects} onSearch={handleTypeaheadSearch} onChange={props.onChange} + disabled={props.disabled ?? false} /> ); }; diff --git a/source/frontend/src/components/common/mapFSM/MapStateMachineContext.tsx b/source/frontend/src/components/common/mapFSM/MapStateMachineContext.tsx index 403d978a51..f8bc731210 100644 --- a/source/frontend/src/components/common/mapFSM/MapStateMachineContext.tsx +++ b/source/frontend/src/components/common/mapFSM/MapStateMachineContext.tsx @@ -38,6 +38,7 @@ export interface IMapStateMachineContext { requestedFlyTo: RequestedFlyTo; requestedCenterTo: RequestedCenterTo; mapMarkerSelected: MarkerSelected | null; + mapMarkedLocation: LatLngLiteral | null; mapLocationSelected: LatLngLiteral | null; mapLocationFeatureDataset: LocationFeatureDataset | null; selectedFeatureDataset: SelectedFeatureDataset | null; @@ -78,6 +79,8 @@ export interface IMapStateMachineContext { mapClick: (latlng: LatLngLiteral) => void; mapMarkerClick: (featureSelected: MarkerSelected) => void; + mapMarkLocation: (laLng: LatLngLiteral) => void; + mapClearLocationMark: () => void; setMapSearchCriteria: (searchCriteria: IPropertyFilter) => void; refreshMapProperties: () => void; @@ -228,6 +231,22 @@ export const MapStateMachineProvider: React.FC> }); }, [serviceSend]); + const mapMarkLocation = useCallback( + (latlng: LatLngLiteral) => { + serviceSend({ + type: 'MAP_MARK_LOCATION', + latlng, + }); + }, + [serviceSend], + ); + + const mapClearLocationMark = useCallback(() => { + serviceSend({ + type: 'MAP_CLEAR_MARK_LOCATION', + }); + }, [serviceSend]); + const mapClick = useCallback( (latlng: LatLngLiteral) => { serviceSend({ @@ -468,6 +487,7 @@ export const MapStateMachineProvider: React.FC> requestedCenterTo: state.context.requestedCenterTo, mapMarkerSelected: state.context.mapFeatureSelected, mapLocationSelected: state.context.mapLocationSelected, + mapMarkedLocation: state.context.mapMarkedLocation, selectedFeatureDataset: state.context.selectedFeatureDataset, mapLocationFeatureDataset: state.context.mapLocationFeatureDataset, repositioningFeatureDataset: state.context.repositioningFeatureDataset, @@ -501,6 +521,8 @@ export const MapStateMachineProvider: React.FC> processFitBounds, openSidebar, closeSidebar, + mapMarkLocation, + mapClearLocationMark, requestFlyToLocation, requestCenterToLocation, requestFlyToBounds, diff --git a/source/frontend/src/components/common/mapFSM/machineDefinition/mapMachine.ts b/source/frontend/src/components/common/mapFSM/machineDefinition/mapMachine.ts index eacd60fd97..080fc1593e 100644 --- a/source/frontend/src/components/common/mapFSM/machineDefinition/mapMachine.ts +++ b/source/frontend/src/components/common/mapFSM/machineDefinition/mapMachine.ts @@ -1,5 +1,5 @@ import { FeatureCollection, Geometry } from 'geojson'; -import { geoJSON, latLngBounds } from 'leaflet'; +import { geoJSON } from 'leaflet'; import { AnyEventObject, assign, createMachine, raise, send } from 'xstate'; import { defaultBounds } from '@/components/maps/constants'; @@ -7,7 +7,6 @@ import { PropertyFilterFormModel } from '@/components/maps/leaflet/Control/Advan import { pimsBoundaryLayers } from '@/components/maps/leaflet/Control/LayersControl/LayerDefinitions'; import { initialEnabledLayers } from '@/components/maps/leaflet/Control/LayersControl/LayersMenuLayout'; import { defaultPropertyFilter } from '@/features/properties/filter/IPropertyFilter'; -import { exists } from '@/utils'; import { emptyFeatureData, LocationBoundaryDataset } from '../models'; import { MachineContext, SideBarType } from './types'; @@ -49,7 +48,6 @@ const featureViewStates = { event: AnyEventObject & { locations: LocationBoundaryDataset[] }, ) => event.locations ?? [], }), - raise('REQUEST_FIT_FILE_BOUNDS'), ], }, }, @@ -74,7 +72,6 @@ const featureViewStates = { event: AnyEventObject & { locations: LocationBoundaryDataset[] }, ) => event.locations ?? [], }), - raise('REQUEST_FIT_FILE_BOUNDS'), ], }, }, @@ -95,7 +92,6 @@ const featureDataLoaderStates = { actions: assign({ isLoading: () => true, searchCriteria: (_, event: any) => event.searchCriteria, - fitToResultsAfterLoading: () => true, }), target: 'loading', }, @@ -109,24 +105,10 @@ const featureDataLoaderStates = { src: 'loadFeatures', onDone: [ { - cond: (context: MachineContext) => context.fitToResultsAfterLoading === true, actions: [ - raise('REQUEST_FIT_BOUNDS'), assign({ isLoading: () => false, mapFeatureData: (_, event: any) => event.data, - fitToResultsAfterLoading: () => false, - mapLayersToRefresh: () => pimsBoundaryLayers, - }), - ], - target: 'idle', - }, - { - actions: [ - assign({ - isLoading: () => false, - mapFeatureData: (_, event: any) => event.data, - fitToResultsAfterLoading: () => false, mapLayersToRefresh: () => pimsBoundaryLayers, }), ], @@ -216,20 +198,6 @@ const mapRequestStates = { }), target: 'pendingFitBounds', }, - REQUEST_FIT_FILE_BOUNDS: { - actions: assign({ - requestedFitBounds: (context: MachineContext) => { - // zoom to the bounds that include all file properties - if (context.filePropertyLocations.length > 0) { - const locations = (context.filePropertyLocations ?? []) - .map(pl => pl.location) - .filter(exists); - return latLngBounds(locations); - } - }, - }), - target: 'pendingFitBounds', - }, }, }, pendingFlyTo: { @@ -290,17 +258,29 @@ const selectedFeatureLoaderStates = { actions: [ assign({ isLoading: () => true, - mapLocationSelected: () => null, mapFeatureSelected: (_, event: any) => event.featureSelected, mapLocationFeatureDataset: () => null, }), ], target: 'loading', }, + MAP_MARK_LOCATION: { + actions: [ + assign({ + mapMarkedLocation: (_, event: any) => event.latlng, + }), + ], + }, + MAP_CLEAR_MARK_LOCATION: { + actions: [ + assign({ + mapMarkedLocation: () => null, + }), + ], + }, CLOSE_POPUP: { actions: [ assign({ - mapLocationSelected: () => null, mapFeatureSelected: () => null, mapLocationFeatureDataset: () => null, }), @@ -404,7 +384,6 @@ const sideBarStates = { event: AnyEventObject & { locations: LocationBoundaryDataset[] }, ) => event.locations ?? [], }), - raise('REQUEST_FIT_FILE_BOUNDS'), ], }, @@ -520,12 +499,12 @@ export const mapMachine = createMachine({ mapLocationSelected: null, mapFeatureSelected: null, mapLocationFeatureDataset: null, + mapMarkedLocation: null, selectedFeatureDataset: null, repositioningFeatureDataset: null, repositioningPropertyIndex: null, selectingComponentId: null, isLoading: false, - fitToResultsAfterLoading: false, searchCriteria: null, advancedSearchCriteria: new PropertyFilterFormModel(), mapFeatureData: emptyFeatureData, diff --git a/source/frontend/src/components/common/mapFSM/machineDefinition/types.ts b/source/frontend/src/components/common/mapFSM/machineDefinition/types.ts index 32de3a1bfe..f2bc912892 100644 --- a/source/frontend/src/components/common/mapFSM/machineDefinition/types.ts +++ b/source/frontend/src/components/common/mapFSM/machineDefinition/types.ts @@ -32,6 +32,7 @@ export type MachineContext = { mapFeatureSelected: MarkerSelected | null; mapLocationSelected: LatLngLiteral | null; mapLocationFeatureDataset: LocationFeatureDataset | null; + mapMarkedLocation: LatLngLiteral | null; selectedFeatureDataset: SelectedFeatureDataset | null; repositioningFeatureDataset: SelectedFeatureDataset | null; repositioningPropertyIndex: number | null; @@ -44,7 +45,6 @@ export type MachineContext = { advancedSearchCriteria: PropertyFilterFormModel | null; isLoading: boolean; - fitToResultsAfterLoading: boolean; requestedFitBounds: LatLngBounds; requestedFlyTo: RequestedFlyTo; requestedCenterTo: RequestedCenterTo; diff --git a/source/frontend/src/components/common/mapFSM/models.ts b/source/frontend/src/components/common/mapFSM/models.ts index e0459c3f23..c278b3c354 100644 --- a/source/frontend/src/components/common/mapFSM/models.ts +++ b/source/frontend/src/components/common/mapFSM/models.ts @@ -36,6 +36,7 @@ export interface RequestedCenterTo { export interface LocationBoundaryDataset { readonly location: LatLngLiteral; readonly boundary: Geometry | null; + readonly isActive?: boolean; } export const emptyPimsLocationFeatureCollection: FeatureCollection< diff --git a/source/frontend/src/components/common/mapFSM/useLocationFeatureLoader.tsx b/source/frontend/src/components/common/mapFSM/useLocationFeatureLoader.tsx index 17f795d8c5..220e64be89 100644 --- a/source/frontend/src/components/common/mapFSM/useLocationFeatureLoader.tsx +++ b/source/frontend/src/components/common/mapFSM/useLocationFeatureLoader.tsx @@ -60,6 +60,8 @@ export interface SelectedFeatureDataset extends FeatureDataset { regionFeature: Feature | null; districtFeature: Feature | null; municipalityFeature: Feature | null; + isActive?: boolean; + displayOrder?: number; } const useLocationFeatureLoader = () => { diff --git a/source/frontend/src/components/common/styles.ts b/source/frontend/src/components/common/styles.ts index 0879bb6ff2..523c1a6495 100644 --- a/source/frontend/src/components/common/styles.ts +++ b/source/frontend/src/components/common/styles.ts @@ -253,3 +253,21 @@ export const ListPage = styled.div` gap: 2.5rem; padding: 0; `; + +export const StyledIconWrapper = styled.div` + &.selected { + background-color: ${props => props.theme.bcTokens.themeGold100}; + } + + background-color: ${props => props.theme.css.numberBackgroundColor}; + font-size: 1.5rem; + border-radius: 50%; + opacity: 0.8; + width: 3.25rem; + height: 3.25rem; + padding: 1rem; + display: flex; + justify-content: center; + align-items: center; + font-family: 'BCSans-Bold'; +`; diff --git a/source/frontend/src/components/maps/MapLeafletView.tsx b/source/frontend/src/components/maps/MapLeafletView.tsx index 75df81d8ce..42c6b2aa9c 100644 --- a/source/frontend/src/components/maps/MapLeafletView.tsx +++ b/source/frontend/src/components/maps/MapLeafletView.tsx @@ -33,8 +33,8 @@ import { LegendControl } from './leaflet/Control/Legend/LegendControl'; import { ZoomOutButton } from './leaflet/Control/ZoomOut/ZoomOutButton'; import { LocationPopupContainer } from './leaflet/LayerPopup/LocationPopupContainer'; import { FilePropertiesLayer } from './leaflet/Layers/FilePropertiesLayer'; -import { InventoryLayer } from './leaflet/Layers/InventoryLayer'; import { LeafletLayerListener } from './leaflet/Layers/LeafletLayerListener'; +import { MarkerLayer } from './leaflet/Layers/MarkerLayer'; import { MapEvents } from './leaflet/MapEvents/MapEvents'; import * as Styled from './leaflet/styles'; import { EsriVectorTileLayer } from './leaflet/VectorTileLayer/EsriVectorTileLayer'; @@ -169,13 +169,12 @@ const MapLeafletView: React.FC> = ( useEffect(() => { if (hasPendingFlyTo && isMapReady) { if (requestedFlyTo.bounds !== null) { - mapRef?.current?.flyToBounds(requestedFlyTo.bounds, { animate: false }); + mapRef?.current?.flyToBounds(requestedFlyTo.bounds, { animate: true }); } if (requestedFlyTo.location !== null) { mapRef?.current?.flyTo(requestedFlyTo.location, MAP_MAX_ZOOM, { - animate: false, + animate: true, }); - mapRef?.current?.panTo(requestedFlyTo.location); } mapMachineProcessFlyTo(); @@ -280,11 +279,11 @@ const MapLeafletView: React.FC> = ( active={mapMachine.isFiltering} /> - + /> {/* Client-side "layer" to highlight file property boundaries (when in the context of a file) */} diff --git a/source/frontend/src/components/maps/MapSearch.tsx b/source/frontend/src/components/maps/MapSearch.tsx index 6208b6a9b0..5e92e47a05 100644 --- a/source/frontend/src/components/maps/MapSearch.tsx +++ b/source/frontend/src/components/maps/MapSearch.tsx @@ -19,8 +19,14 @@ export type MapSearchProps = object; * @param param0 */ const MapSearch: React.FC> = () => { - const { mapSearchCriteria, setMapSearchCriteria, mapClick, requestCenterToLocation } = - useMapStateMachine(); + const { + mapSearchCriteria, + setMapSearchCriteria, + mapClick, + requestCenterToLocation, + mapMarkLocation, + mapClearLocationMark, + } = useMapStateMachine(); const [propertySearchFilter, setPropertySearchFilter] = useState(null); @@ -46,10 +52,12 @@ const MapSearch: React.FC> = () => { latLng = filter.coordinates?.toLatLng(); } if (latLng) { - mapClick(latLng); + mapMarkLocation(latLng); requestCenterToLocation(latLng); + mapClick(latLng); } } else { + mapClearLocationMark(); setPropertySearchFilter(filter); } }; diff --git a/source/frontend/src/components/maps/leaflet/Control/LayersControl/LayerDefinitions.ts b/source/frontend/src/components/maps/leaflet/Control/LayersControl/LayerDefinitions.ts index 7faafbb8ea..24a805fd42 100644 --- a/source/frontend/src/components/maps/leaflet/Control/LayersControl/LayerDefinitions.ts +++ b/source/frontend/src/components/maps/leaflet/Control/LayersControl/LayerDefinitions.ts @@ -345,6 +345,39 @@ export const layerDefinitions: LayerDefinition[] = [ maxNativeZoom: MAP_MAX_NATIVE_ZOOM, maxZoom: MAP_MAX_ZOOM, }, + { + layerIdentifier: 'pmbc_parcel_pid', + url: 'https://openmaps.gov.bc.ca/geo/pub/WHSE_CADASTRE.PMBC_PARCEL_FABRIC_POLY_SVW/ows?', + layers: 'pub:WHSE_CADASTRE.PMBC_PARCEL_FABRIC_POLY_SVW', + transparent: true, + format: 'image/png', + zIndex: 9999, + styles: '7834', + maxNativeZoom: MAP_MAX_NATIVE_ZOOM, + maxZoom: MAP_MAX_ZOOM, + }, + { + layerIdentifier: 'pmbc_parcel_by_class', + url: 'https://openmaps.gov.bc.ca/geo/pub/WHSE_CADASTRE.PMBC_PARCEL_FABRIC_POLY_SVW/ows?', + layers: 'pub:WHSE_CADASTRE.PMBC_PARCEL_FABRIC_POLY_SVW', + transparent: true, + format: 'image/png', + zIndex: 21, + styles: '7943', + maxNativeZoom: MAP_MAX_NATIVE_ZOOM, + maxZoom: MAP_MAX_ZOOM, + }, + { + layerIdentifier: 'pmbc_parcel_by_owner', + url: 'https://openmaps.gov.bc.ca/geo/pub/WHSE_CADASTRE.PMBC_PARCEL_FABRIC_POLY_SVW/ows?', + layers: 'pub:WHSE_CADASTRE.PMBC_PARCEL_FABRIC_POLY_SVW', + transparent: true, + format: 'image/png', + zIndex: 20, + styles: '6616', + maxNativeZoom: MAP_MAX_NATIVE_ZOOM, + maxZoom: MAP_MAX_ZOOM, + }, { layerIdentifier: 'bctfa_property', layers: 'psp:PMBC_BCTFA_PARCEL_POLYGON_FABRIC', diff --git a/source/frontend/src/components/maps/leaflet/Control/LayersControl/LayersMenuLayout.tsx b/source/frontend/src/components/maps/leaflet/Control/LayersControl/LayersMenuLayout.tsx index 40f0526742..dd778f6735 100644 --- a/source/frontend/src/components/maps/leaflet/Control/LayersControl/LayersMenuLayout.tsx +++ b/source/frontend/src/components/maps/leaflet/Control/LayersControl/LayersMenuLayout.tsx @@ -94,6 +94,11 @@ export const layersMenuTree: LayerMenuEntry = { key: 'external_layers', label: 'External', nodes: [ + { + layerDefinitionId: 'pmbc_parcel_pid', + key: 'pmbc_parcel_pid', + label: 'Parcels PID', + }, { key: 'administrative_group', label: 'Administrative Boundaries', @@ -250,12 +255,6 @@ export const layersMenuTree: LayerMenuEntry = { label: 'Parcel Boundaries', color: '#E9AD34', }, - { - layerDefinitionId: 'parcelLayerStyled', - key: 'parcelLayerStyled', - label: 'Parcel Boundaries Styled', - color: '#ff9800', - }, { layerDefinitionId: 'srwInterestParcels', key: 'srwInterestParcels', @@ -268,6 +267,16 @@ export const layersMenuTree: LayerMenuEntry = { label: 'BCTFA Ownership', color: '#42814A', }, + { + layerDefinitionId: 'pmbc_parcel_by_class', + key: 'pmbc_parcel_by_class', + label: 'Parcels By Class (multiple colors)', + }, + { + layerDefinitionId: 'pmbc_parcel_by_owner', + key: 'pmbc_parcel_by_owner', + label: 'Parcels By Owner Type (multiple colors)', + }, ], }, { diff --git a/source/frontend/src/components/maps/leaflet/Control/LayersControl/__snapshots__/LayersMenu.test.tsx.snap b/source/frontend/src/components/maps/leaflet/Control/LayersControl/__snapshots__/LayersMenu.test.tsx.snap index bff0d724c2..64b45b594a 100644 --- a/source/frontend/src/components/maps/leaflet/Control/LayersControl/__snapshots__/LayersMenu.test.tsx.snap +++ b/source/frontend/src/components/maps/leaflet/Control/LayersControl/__snapshots__/LayersMenu.test.tsx.snap @@ -360,6 +360,26 @@ exports[`LayersMenu View > renders as expected 1`] = `
+
+
+ + +
+
diff --git a/source/frontend/src/components/maps/leaflet/LayerPopup/LayerPopupView.tsx b/source/frontend/src/components/maps/leaflet/LayerPopup/LayerPopupView.tsx index 57a6fc5ad4..3ec73fc8ec 100644 --- a/source/frontend/src/components/maps/leaflet/LayerPopup/LayerPopupView.tsx +++ b/source/frontend/src/components/maps/leaflet/LayerPopup/LayerPopupView.tsx @@ -184,6 +184,7 @@ export const LayerPopupView: React.FC = { requestFlyToBounds: vi.fn(), + requestFlyToLocation: vi.fn(), }; const northEast = new L.LatLng(50.5, -120.7); @@ -35,6 +36,7 @@ describe('Layer Popup links', () => { bounds: new L.LatLngBounds(southWest, northEast), onViewPropertyInfo: onViewPropertyInfoFn, showViewPropertyInfo: true, + latLng: undefined, }); expect(asFragment()).toMatchSnapshot(); }); @@ -44,6 +46,7 @@ describe('Layer Popup links', () => { bounds: new L.LatLngBounds(southWest, northEast), onViewPropertyInfo: onViewPropertyInfoFn, showViewPropertyInfo: true, + latLng: undefined, }); const link = getByText(/Zoom/i); expect(link).toBeInTheDocument(); @@ -55,6 +58,7 @@ describe('Layer Popup links', () => { bounds: new L.LatLngBounds(southWest, northEast), onViewPropertyInfo: onViewPropertyInfoFn, showViewPropertyInfo: true, + latLng: undefined, }); const link = getByText(/Zoom/i); expect(link).toBeInTheDocument(); @@ -63,11 +67,27 @@ describe('Layer Popup links', () => { expect(mapMachineMock.requestFlyToBounds).toBeCalled(); }); + it(`Zooms the map to property location when Zoom link is clicked`, async () => { + // render popup + const { getByText } = renderLinks({ + bounds: undefined, + onViewPropertyInfo: onViewPropertyInfoFn, + showViewPropertyInfo: true, + latLng: southWest, + }); + const link = getByText(/Zoom/i); + expect(link).toBeInTheDocument(); + // click link + await act(async () => userEvent.click(link)); + expect(mapMachineMock.requestFlyToLocation).toBeCalled(); + }); + it('calls onViewPropertyInfo when link clicked', async () => { const { getByText } = renderLinks({ bounds: new L.LatLngBounds(southWest, northEast), onViewPropertyInfo: onViewPropertyInfoFn, showViewPropertyInfo: true, + latLng: undefined, }); const link = getByText('View Property Info'); diff --git a/source/frontend/src/components/maps/leaflet/LayerPopup/components/LayerPopupLinks.tsx b/source/frontend/src/components/maps/leaflet/LayerPopup/components/LayerPopupLinks.tsx index c5cdbaa313..9eb1d81307 100644 --- a/source/frontend/src/components/maps/leaflet/LayerPopup/components/LayerPopupLinks.tsx +++ b/source/frontend/src/components/maps/leaflet/LayerPopup/components/LayerPopupLinks.tsx @@ -1,4 +1,4 @@ -import { LatLngBounds } from 'leaflet'; +import { LatLngBounds, LatLngLiteral } from 'leaflet'; import noop from 'lodash/noop'; import React, { useCallback } from 'react'; import { FaEllipsisH, FaEye, FaSearchPlus } from 'react-icons/fa'; @@ -7,9 +7,11 @@ import styled from 'styled-components'; import { LinkButton } from '@/components/common/buttons'; import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; import TooltipWrapper from '@/components/common/TooltipWrapper'; +import { exists } from '@/utils/utils'; export interface ILayerPopupLinksProps { bounds: LatLngBounds | undefined; + latLng: LatLngLiteral | undefined; onEllipsisClick?: () => void; onViewPropertyInfo: (event: React.MouseEvent) => void; showViewPropertyInfo: boolean; @@ -17,17 +19,20 @@ export interface ILayerPopupLinksProps { export const LayerPopupLinks: React.FC> = ({ bounds, + latLng, onViewPropertyInfo, onEllipsisClick, showViewPropertyInfo, }) => { - const { requestFlyToBounds } = useMapStateMachine(); + const { requestFlyToBounds, requestFlyToLocation } = useMapStateMachine(); const onZoomToBounds = useCallback(() => { - if (bounds !== undefined) { + if (exists(bounds)) { requestFlyToBounds(bounds); + } else if (exists(latLng)) { + requestFlyToLocation(latLng); } - }, [requestFlyToBounds, bounds]); + }, [bounds, latLng, requestFlyToBounds, requestFlyToLocation]); return ( diff --git a/source/frontend/src/components/maps/leaflet/Layers/InventoryLayer.tsx b/source/frontend/src/components/maps/leaflet/Layers/InventoryLayer.tsx deleted file mode 100644 index 2b7a65a0a5..0000000000 --- a/source/frontend/src/components/maps/leaflet/Layers/InventoryLayer.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { BBox } from 'geojson'; -import { LatLngBounds } from 'leaflet'; -import React, { useMemo } from 'react'; -import { useMap } from 'react-leaflet'; -import { tilesInBbox } from 'tiles-in-bbox'; - -import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; - -import { PointFeature } from '../../types'; -import PointClusterer from './PointClusterer'; - -export type InventoryLayerProps = { - /** Latitude and Longitude boundary of the layer. */ - bounds: LatLngBounds; - /** Zoom level of the map. */ - zoom: number; - /** Minimum zoom level allowed. */ - minZoom?: number; - /** Maximum zoom level allowed. */ - maxZoom?: number; -}; - -/** - * Get a new instance of a BBox from the specified 'bounds'. - * @param bounds The latitude longitude boundary. - */ -const getBbox = (bounds: LatLngBounds): BBox => { - return [ - bounds.getSouthWest().lng, - bounds.getSouthWest().lat, - bounds.getNorthEast().lng, - bounds.getNorthEast().lat, - ]; -}; - -interface ITilePoint { - // x axis of the tile - x: number; - // y axis of the tile - y: number; - // zoom state of the tile - z: number; -} - -interface ITile { - // Tile point {x, y, z} - point: ITilePoint; - // unique id of the file - key: string; - // bbox of the tile - bbox: string; - // tile data status - processed?: boolean; - // tile data, a list of properties in the tile - datum?: PointFeature[]; - // tile bounds - latlngBounds: LatLngBounds; -} - -/** - * Generate tiles for current bounds and zoom - * @param bounds - * @param zoom - */ -export const getTiles = (bounds: LatLngBounds, zoom: number): ITile[] => { - const bbox = { - bottom: bounds.getSouth(), - left: bounds.getWest(), - top: bounds.getNorth(), - right: bounds.getEast(), - }; - - const tiles = tilesInBbox(bbox, zoom); - - // convert tile x axis to longitude - const tileToLong = (x: number, z: number) => { - return (x / Math.pow(2, z)) * 360 - 180; - }; - - // convert tile y axis to longitude - const tileToLat = (y: number, z: number) => { - const n = Math.PI - (2 * Math.PI * y) / Math.pow(2, z); - - return (180 / Math.PI) * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n))); - }; - - return tiles.map(({ x, y, z }) => { - const SW_long = tileToLong(x, z); - - const SW_lat = tileToLat(y + 1, z); - - const NE_long = tileToLong(x + 1, z); - - const NE_lat = tileToLat(y, z); - - return { - key: `${x}:${y}:${z}`, - bbox: SW_long + ',' + SW_lat + ',' + NE_long + ',' + NE_lat + ',EPSG:4326', - point: { x, y, z }, - datum: [], - latlngBounds: new LatLngBounds({ lat: SW_lat, lng: SW_long }, { lat: NE_lat, lng: NE_long }), - }; - }); -}; - -/** - * Displays the search results onto a layer with clustering. - * This component makes a request to the PIMS API properties search WFS endpoint. - */ -export const InventoryLayer: React.FC> = ({ - bounds, - zoom, - minZoom, - maxZoom, -}) => { - const mapInstance = useMap(); - const mapMachine = useMapStateMachine(); - - if (!mapInstance) { - throw new Error(' must be used under a leaflet component'); - } - - const bbox = useMemo(() => getBbox(bounds), [bounds]); - - return ( - - ); -}; diff --git a/source/frontend/src/components/maps/leaflet/Layers/MarkerLayer.tsx b/source/frontend/src/components/maps/leaflet/Layers/MarkerLayer.tsx new file mode 100644 index 0000000000..dee6fd25d9 --- /dev/null +++ b/source/frontend/src/components/maps/leaflet/Layers/MarkerLayer.tsx @@ -0,0 +1,88 @@ +import { BBox } from 'geojson'; +import { LatLngBounds, LeafletMouseEvent } from 'leaflet'; +import React, { useMemo } from 'react'; +import { FeatureGroup, Marker, useMap } from 'react-leaflet'; + +import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; +import { exists } from '@/utils'; + +import PointClusterer from './PointClusterer'; +import { getNotOwnerMarkerIcon } from './util'; + +export type InventoryLayerProps = { + /** Latitude and Longitude boundary of the layer. */ + bounds: LatLngBounds; + /** Zoom level of the map. */ + zoom: number; + /** Minimum zoom level allowed. */ + minZoom?: number; + /** Maximum zoom level allowed. */ + maxZoom?: number; +}; + +/** + * Get a new instance of a BBox from the specified 'bounds'. + * @param bounds The latitude longitude boundary. + */ +const getBbox = (bounds: LatLngBounds): BBox => { + return [ + bounds.getSouthWest().lng, + bounds.getSouthWest().lat, + bounds.getNorthEast().lng, + bounds.getNorthEast().lat, + ]; +}; + +/** + * Displays the search results onto a layer with clustering. + * This component makes a request to the PIMS API properties search WFS endpoint. + */ +export const MarkerLayer: React.FC> = ({ + bounds, + zoom, + minZoom, + maxZoom, +}) => { + const mapInstance = useMap(); + const mapMachine = useMapStateMachine(); + + if (!mapInstance) { + throw new Error(' must be used under a leaflet component'); + } + + const bbox = useMemo(() => getBbox(bounds), [bounds]); + + const markedLocation = useMemo( + () => mapMachine.mapMarkedLocation, + [mapMachine.mapMarkedLocation], + ); + + return ( + <> + + + {exists(markedLocation) && ( + + { + e.originalEvent.stopPropagation(); + mapMachine.mapClick(markedLocation); + }, + }} + /> + + )} + + ); +}; diff --git a/source/frontend/src/components/maps/leaflet/Layers/util.tsx b/source/frontend/src/components/maps/leaflet/Layers/util.tsx index d8beb25384..26d346e303 100644 --- a/source/frontend/src/components/maps/leaflet/Layers/util.tsx +++ b/source/frontend/src/components/maps/leaflet/Layers/util.tsx @@ -16,6 +16,7 @@ import otherInterestImage from '@/assets/images/pins/other-interest.png'; import otherInterestHighlightImage from '@/assets/images/pins/other-interest-highlight.png'; import retiredImage from '@/assets/images/pins/retired.png'; import { ICluster } from '@/components/maps/types'; +import DisabledDraftCircleNumber from '@/components/propertySelector/selectedPropertyList/DisabledDraftCircleNumber'; import { DraftCircleNumber } from '@/components/propertySelector/selectedPropertyList/DraftCircleNumber'; import { PMBC_FullyAttributed_Feature_Properties } from '@/models/layers/parcelMapBC'; import { @@ -223,7 +224,7 @@ export function getNotOwnerMarkerIcon(selected: boolean): L.Icon return notOwnedPropertyIcon; } -// parcel icon (green) highlighted +// parcel icon (blue with number) highlighted export const getDraftIcon = (text: string) => { return L.divIcon({ iconSize: [29, 45], @@ -234,6 +235,17 @@ export const getDraftIcon = (text: string) => { }); }; +// disabled parcel icon (grey with number) highlighted +export const getDisabledDraftIcon = (text: string) => { + return L.divIcon({ + iconSize: [29, 40], + iconAnchor: [15, 40], + popupAnchor: [1, -34], + shadowSize: [41, 41], + html: ReactDOMServer.renderToStaticMarkup(), + }); +}; + /** * Creates a map pin for a single point; e.g. a parcel * @param feature the geojson object diff --git a/source/frontend/src/components/maps/leaflet/SvgMarkers/DisabledDraftMarker.tsx b/source/frontend/src/components/maps/leaflet/SvgMarkers/DisabledDraftMarker.tsx new file mode 100644 index 0000000000..4a1b3e3751 --- /dev/null +++ b/source/frontend/src/components/maps/leaflet/SvgMarkers/DisabledDraftMarker.tsx @@ -0,0 +1,51 @@ +import styled from 'styled-components'; + +interface IDraftMarkerProps { + text?: string; +} + +export const DisabledDraftMarker: React.FunctionComponent< + React.PropsWithChildren +> = ({ text, children }) => { + return ( + + + + + + + + + + {text} + {children} + + ); +}; + +const StyledMarker = styled.svg` + min-width: 2.9rem; + max-width: 2.9rem;1a +`; + +export default DisabledDraftMarker; diff --git a/source/frontend/src/components/propertySelector/MapClickMonitor.tsx b/source/frontend/src/components/propertySelector/MapClickMonitor.tsx index c9b6a81217..486f7eb3d0 100644 --- a/source/frontend/src/components/propertySelector/MapClickMonitor.tsx +++ b/source/frontend/src/components/propertySelector/MapClickMonitor.tsx @@ -6,7 +6,7 @@ import useDraftMarkerSynchronizer from '@/hooks/useDraftMarkerSynchronizer'; import { usePrevious } from '@/hooks/usePrevious'; import useDeepCompareEffect from '@/hooks/util/useDeepCompareEffect'; import { exists, firstOrNull, isValidId } from '@/utils'; -import { featuresetToMapProperty } from '@/utils/mapPropertyUtils'; +import { featuresetToLocationBoundaryDataset } from '@/utils/mapPropertyUtils'; import { SelectedFeatureDataset } from '../common/mapFSM/useLocationFeatureLoader'; @@ -30,7 +30,9 @@ export const MapClickMonitor: React.FunctionComponent = ( const mapMachine = useMapStateMachine(); const previous = usePrevious(mapMachine.mapLocationFeatureDataset); - const modifiedMapProperties = modifiedProperties.map(mp => featuresetToMapProperty(mp)); + const modifiedMapProperties = modifiedProperties.map(mp => + featuresetToLocationBoundaryDataset(mp), + ); useDraftMarkerSynchronizer(selectedComponentId ? [] : modifiedMapProperties); // disable the draft marker synchronizer if the selecting component is set - the parent will need to control the draft markers. useDeepCompareEffect(() => { diff --git a/source/frontend/src/components/propertySelector/models.ts b/source/frontend/src/components/propertySelector/models.ts index bf36f3b46d..355294653b 100644 --- a/source/frontend/src/components/propertySelector/models.ts +++ b/source/frontend/src/components/propertySelector/models.ts @@ -21,6 +21,7 @@ export interface IMapProperty { districtName?: string; landArea?: number; areaUnit?: AreaUnitTypes; + isActive?: boolean; } export interface ILayerSearchCriteria { diff --git a/source/frontend/src/components/propertySelector/selectedPropertyList/DisabledDraftCircleNumber.tsx b/source/frontend/src/components/propertySelector/selectedPropertyList/DisabledDraftCircleNumber.tsx new file mode 100644 index 0000000000..f21a6c6f98 --- /dev/null +++ b/source/frontend/src/components/propertySelector/selectedPropertyList/DisabledDraftCircleNumber.tsx @@ -0,0 +1,28 @@ +import styled from 'styled-components'; + +import DisabledDraftMarker from '@/components/maps/leaflet/SvgMarkers/DisabledDraftMarker'; + +interface IDisabledDraftCircleNumberProps { + text?: string; +} + +export const DisabledDraftCircleNumber: React.FunctionComponent< + React.PropsWithChildren +> = ({ text }) => { + return ( + + + {text?.length ?? 0 <= 2 ? text : '..'} + + + ); +}; + +const StyledText = styled.text` + font-size: 6rem; + fill: black; + text-anchor: middle; + alignment-baseline: central; +`; + +export default DisabledDraftCircleNumber; diff --git a/source/frontend/src/components/propertySelector/selectedPropertyList/SelectedPropertyRow.test.tsx b/source/frontend/src/components/propertySelector/selectedPropertyList/SelectedPropertyRow.test.tsx index ccfd482f8c..b6d6910735 100644 --- a/source/frontend/src/components/propertySelector/selectedPropertyList/SelectedPropertyRow.test.tsx +++ b/source/frontend/src/components/propertySelector/selectedPropertyList/SelectedPropertyRow.test.tsx @@ -40,6 +40,8 @@ describe('SelectedPropertyRow component', () => { } index={renderOptions.index ?? 0} onRemove={onRemove} + showDisable={renderOptions?.showDisable} + nameSpace="properties.0" /> )} , @@ -144,4 +146,38 @@ describe('SelectedPropertyRow component', () => { await act(async () => {}); expect(getByText('Address: a test address')).toBeVisible(); }); + + it('shows Inactive as selected when isActive is false', async () => { + const mapProperties: IMapProperty[] = [ + { + pid: '111111111', + latitude: 4, + longitude: 5, + isActive: false, + }, + ]; + const { getByDisplayValue } = setup({ + values: { properties: mapProperties }, + showDisable: true, + }); + await act(async () => {}); + expect(getByDisplayValue('Inactive')).toBeInTheDocument(); + }); + + it('shows Active as selected when isActive is true', async () => { + const mapProperties: IMapProperty[] = [ + { + pid: '111111111', + latitude: 4, + longitude: 5, + isActive: true, + }, + ]; + const { getByDisplayValue } = setup({ + values: { properties: mapProperties }, + showDisable: true, + }); + await act(async () => {}); + expect(getByDisplayValue('Active')).toBeInTheDocument(); + }); }); diff --git a/source/frontend/src/components/propertySelector/selectedPropertyList/SelectedPropertyRow.tsx b/source/frontend/src/components/propertySelector/selectedPropertyList/SelectedPropertyRow.tsx index fc606cad60..26e7cc3c3d 100644 --- a/source/frontend/src/components/propertySelector/selectedPropertyList/SelectedPropertyRow.tsx +++ b/source/frontend/src/components/propertySelector/selectedPropertyList/SelectedPropertyRow.tsx @@ -1,21 +1,29 @@ import { getIn, useFormikContext } from 'formik'; -import { useEffect } from 'react'; +import { geoJSON } from 'leaflet'; +import { useCallback, useEffect } from 'react'; import { Col, Row } from 'react-bootstrap'; +import { FaSearchPlus } from 'react-icons/fa'; import { RiDragMove2Line } from 'react-icons/ri'; +import styled from 'styled-components'; -import { RemoveButton, StyledIconButton } from '@/components/common/buttons'; +import { LinkButton, RemoveButton, StyledIconButton } from '@/components/common/buttons'; +import { Select } from '@/components/common/form'; import { InlineInput } from '@/components/common/form/styles'; import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; import { SelectedFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; import OverflowTip from '@/components/common/OverflowTip'; import DraftCircleNumber from '@/components/propertySelector/selectedPropertyList/DraftCircleNumber'; +import { exists } from '@/utils'; import { withNameSpace } from '@/utils/formUtils'; import { featuresetToMapProperty, getPropertyName, NameSourceType } from '@/utils/mapPropertyUtils'; +import DisabledDraftCircleNumber from './DisabledDraftCircleNumber'; + export interface ISelectedPropertyRowProps { index: number; nameSpace?: string; onRemove: () => void; + showDisable?: boolean; property: SelectedFeatureDataset; } @@ -23,6 +31,7 @@ export const SelectedPropertyRow: React.FunctionComponent { const mapMachine = useMapStateMachine(); @@ -33,6 +42,15 @@ export const SelectedPropertyRow: React.FunctionComponent { + const geom = property.pimsFeature.geometry; + const bounds = geoJSON(geom).getBounds(); + + if (exists(bounds) && bounds.isValid()) { + mapMachine.requestFlyToBounds(bounds); + } + }, [mapMachine, property.pimsFeature.geometry]); + const propertyName = getPropertyName(featuresetToMapProperty(property)); let propertyIdentifier = ''; switch (propertyName.label) { @@ -46,14 +64,17 @@ export const SelectedPropertyRow: React.FunctionComponent +
- + {property.isActive === false ? ( + + ) : ( + + )}
@@ -67,6 +88,23 @@ export const SelectedPropertyRow: React.FunctionComponent + + + + + + {showDisable && ( + + + + )} - + - +
); }; +const StyledRow = styled(Row)` + min-height: 4.5rem; +`; + export default SelectedPropertyRow; diff --git a/source/frontend/src/components/propertySelector/selectedPropertyList/__snapshots__/SelectedPropertyRow.test.tsx.snap b/source/frontend/src/components/propertySelector/selectedPropertyList/__snapshots__/SelectedPropertyRow.test.tsx.snap index 53a3b0400c..80f7486b82 100644 --- a/source/frontend/src/components/propertySelector/selectedPropertyList/__snapshots__/SelectedPropertyRow.test.tsx.snap +++ b/source/frontend/src/components/propertySelector/selectedPropertyList/__snapshots__/SelectedPropertyRow.test.tsx.snap @@ -6,7 +6,7 @@ exports[`SelectedPropertyRow component > renders as expected 1`] = ` class="Toastify" />
- .c4.btn { + .c5.btn { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -35,19 +35,19 @@ exports[`SelectedPropertyRow component > renders as expected 1`] = ` cursor: pointer; } -.c4.btn .Button__value { +.c5.btn .Button__value { width: -webkit-max-content; width: -moz-max-content; width: max-content; } -.c4.btn:hover { +.c5.btn:hover { -webkit-text-decoration: underline; text-decoration: underline; opacity: 0.8; } -.c4.btn:focus { +.c5.btn:focus { outline-width: 2px; outline-style: solid; outline-color: #2E5DD7; @@ -55,31 +55,31 @@ exports[`SelectedPropertyRow component > renders as expected 1`] = ` box-shadow: none; } -.c4.btn.btn-primary { +.c5.btn.btn-primary { color: #FFFFFF; background-color: #013366; } -.c4.btn.btn-primary:hover, -.c4.btn.btn-primary:active, -.c4.btn.btn-primary:focus { +.c5.btn.btn-primary:hover, +.c5.btn.btn-primary:active, +.c5.btn.btn-primary:focus { background-color: #1E5189; } -.c4.btn.btn-secondary { +.c5.btn.btn-secondary { color: #013366; background: none; border-color: #013366; } -.c4.btn.btn-secondary:hover, -.c4.btn.btn-secondary:active, -.c4.btn.btn-secondary:focus { +.c5.btn.btn-secondary:hover, +.c5.btn.btn-secondary:active, +.c5.btn.btn-secondary:focus { color: #FFFFFF; background-color: #013366; } -.c4.btn.btn-info { +.c5.btn.btn-info { color: #9F9D9C; border: none; background: none; @@ -87,66 +87,66 @@ exports[`SelectedPropertyRow component > renders as expected 1`] = ` padding-right: 0.6rem; } -.c4.btn.btn-info:hover, -.c4.btn.btn-info:active, -.c4.btn.btn-info:focus { +.c5.btn.btn-info:hover, +.c5.btn.btn-info:active, +.c5.btn.btn-info:focus { color: var(--surface-color-primary-button-hover); background: none; } -.c4.btn.btn-light { +.c5.btn.btn-light { color: #FFFFFF; background-color: #606060; border: none; } -.c4.btn.btn-light:hover, -.c4.btn.btn-light:active, -.c4.btn.btn-light:focus { +.c5.btn.btn-light:hover, +.c5.btn.btn-light:active, +.c5.btn.btn-light:focus { color: #FFFFFF; background-color: #606060; } -.c4.btn.btn-dark { +.c5.btn.btn-dark { color: #FFFFFF; background-color: #474543; border: none; } -.c4.btn.btn-dark:hover, -.c4.btn.btn-dark:active, -.c4.btn.btn-dark:focus { +.c5.btn.btn-dark:hover, +.c5.btn.btn-dark:active, +.c5.btn.btn-dark:focus { color: #FFFFFF; background-color: #474543; } -.c4.btn.btn-danger { +.c5.btn.btn-danger { color: #FFFFFF; background-color: #CE3E39; } -.c4.btn.btn-danger:hover, -.c4.btn.btn-danger:active, -.c4.btn.btn-danger:focus { +.c5.btn.btn-danger:hover, +.c5.btn.btn-danger:active, +.c5.btn.btn-danger:focus { color: #FFFFFF; background-color: #CE3E39; } -.c4.btn.btn-warning { +.c5.btn.btn-warning { color: #FFFFFF; background-color: #FCBA19; border-color: #FCBA19; } -.c4.btn.btn-warning:hover, -.c4.btn.btn-warning:active, -.c4.btn.btn-warning:focus { +.c5.btn.btn-warning:hover, +.c5.btn.btn-warning:active, +.c5.btn.btn-warning:focus { color: #FFFFFF; border-color: #FCBA19; background-color: #FCBA19; } -.c4.btn.btn-link { +.c5.btn.btn-link { font-size: 1.6rem; font-weight: 400; color: var(--surface-color-primary-button-default); @@ -170,9 +170,9 @@ exports[`SelectedPropertyRow component > renders as expected 1`] = ` text-decoration: underline; } -.c4.btn.btn-link:hover, -.c4.btn.btn-link:active, -.c4.btn.btn-link:focus { +.c5.btn.btn-link:hover, +.c5.btn.btn-link:active, +.c5.btn.btn-link:focus { color: var(--surface-color-primary-button-hover); -webkit-text-decoration: underline; text-decoration: underline; @@ -182,15 +182,15 @@ exports[`SelectedPropertyRow component > renders as expected 1`] = ` outline: none; } -.c4.btn.btn-link:disabled, -.c4.btn.btn-link.disabled { +.c5.btn.btn-link:disabled, +.c5.btn.btn-link.disabled { color: #9F9D9C; background: none; pointer-events: none; } -.c4.btn:disabled, -.c4.btn:disabled:hover { +.c5.btn:disabled, +.c5.btn:disabled:hover { color: #9F9D9C; background-color: #EDEBE9; box-shadow: none; @@ -202,66 +202,66 @@ exports[`SelectedPropertyRow component > renders as expected 1`] = ` cursor: not-allowed; } -.c4.Button .Button__icon { +.c5.Button .Button__icon { margin-right: 1.6rem; } -.c4.Button--icon-only:focus { +.c5.Button--icon-only:focus { outline: none; } -.c4.Button--icon-only .Button__icon { +.c5.Button--icon-only .Button__icon { margin-right: 0; } -.c5.c5.btn { +.c6.c6.btn { background-color: unset; border: none; } -.c5.c5.btn:hover, -.c5.c5.btn:focus, -.c5.c5.btn:active { +.c6.c6.btn:hover, +.c6.c6.btn:focus, +.c6.c6.btn:active { background-color: unset; outline: none; box-shadow: none; } -.c5.c5.btn svg { +.c6.c6.btn svg { -webkit-transition: all 0.3s ease-out; transition: all 0.3s ease-out; } -.c5.c5.btn svg:hover { +.c6.c6.btn svg:hover { -webkit-transition: all 0.3s ease-in; transition: all 0.3s ease-in; } -.c5.c5.btn.btn-primary svg { +.c6.c6.btn.btn-primary svg { color: #013366; } -.c5.c5.btn.btn-primary svg:hover { +.c6.c6.btn.btn-primary svg:hover { color: #013366; } -.c5.c5.btn.btn-light svg { +.c6.c6.btn.btn-light svg { color: var(--surface-color-primary-button-default); } -.c5.c5.btn.btn-light svg:hover { +.c6.c6.btn.btn-light svg:hover { color: #CE3E39; } -.c5.c5.btn.btn-info svg { +.c6.c6.btn.btn-info svg { color: var(--surface-color-primary-button-default); } -.c5.c5.btn.btn-info svg:hover { +.c6.c6.btn.btn-info svg:hover { color: var(--surface-color-primary-button-hover); } -.c6.c6.btn { +.c7.c7.btn { font-size: 1.4rem; color: #aaaaaa; -webkit-text-decoration: none; @@ -269,13 +269,13 @@ exports[`SelectedPropertyRow component > renders as expected 1`] = ` line-height: unset; } -.c6.c6.btn .text { +.c7.c7.btn .text { display: none; } -.c6.c6.btn:hover, -.c6.c6.btn:active, -.c6.c6.btn:focus { +.c7.c7.btn:hover, +.c7.c7.btn:active, +.c7.c7.btn:focus { color: #d8292f; -webkit-text-decoration: none; text-decoration: none; @@ -289,14 +289,14 @@ exports[`SelectedPropertyRow component > renders as expected 1`] = ` flex-direction: row; } -.c6.c6.btn:hover .text, -.c6.c6.btn:active .text, -.c6.c6.btn:focus .text { +.c7.c7.btn:hover .text, +.c7.c7.btn:active .text, +.c7.c7.btn:focus .text { display: inline; line-height: 2rem; } -.c3 { +.c4 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -308,29 +308,33 @@ exports[`SelectedPropertyRow component > renders as expected 1`] = ` gap: 0.8rem; } -.c3 .form-label { +.c4 .form-label { -webkit-flex-shrink: 0; -ms-flex-negative: 0; flex-shrink: 0; } -.c2 { +.c3 { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.c0 { +.c1 { min-width: 3rem; min-height: 4.5rem; } -.c1 { +.c2 { font-size: 1.2rem; } +.c0 { + min-height: 4.5rem; +} +
renders as expected 1`] = ` class="mb-0 d-flex align-items-center" > renders as expected 1`] = ` renders as expected 1`] = `
@@ -417,22 +421,49 @@ exports[`SelectedPropertyRow component > renders as expected 1`] = ` class="col-md-5" >
+
+ +
+
+
diff --git a/source/frontend/src/features/leases/detail/LeaseHeaderAddresses.test.tsx b/source/frontend/src/features/leases/detail/LeaseHeaderAddresses.test.tsx index 10dd874bf7..fda0c00856 100644 --- a/source/frontend/src/features/leases/detail/LeaseHeaderAddresses.test.tsx +++ b/source/frontend/src/features/leases/detail/LeaseHeaderAddresses.test.tsx @@ -65,6 +65,7 @@ describe('LeaseHeaderAddresses component', () => { propertyId: 0, propertyName: null, location: null, + isActive: null, rowVersion: null, }, { @@ -78,6 +79,7 @@ describe('LeaseHeaderAddresses component', () => { propertyId: 0, propertyName: null, location: null, + isActive: null, rowVersion: null, }, { @@ -91,6 +93,7 @@ describe('LeaseHeaderAddresses component', () => { propertyId: 0, propertyName: null, location: null, + isActive: null, rowVersion: null, }, { @@ -104,6 +107,7 @@ describe('LeaseHeaderAddresses component', () => { propertyId: 0, propertyName: null, location: null, + isActive: null, rowVersion: null, }, { @@ -117,6 +121,7 @@ describe('LeaseHeaderAddresses component', () => { propertyId: 0, propertyName: null, location: null, + isActive: null, rowVersion: null, }, ], diff --git a/source/frontend/src/features/leases/detail/LeasePages/surplus/Surplus.test.tsx b/source/frontend/src/features/leases/detail/LeasePages/surplus/Surplus.test.tsx index aa253e6dbe..c8a176b95b 100644 --- a/source/frontend/src/features/leases/detail/LeasePages/surplus/Surplus.test.tsx +++ b/source/frontend/src/features/leases/detail/LeasePages/surplus/Surplus.test.tsx @@ -72,6 +72,7 @@ describe('Lease Surplus Declaration', () => { propertyId: 0, propertyName: null, location: null, + isActive: null, rowVersion: null, }, ], @@ -107,6 +108,7 @@ describe('Lease Surplus Declaration', () => { propertyId: 0, propertyName: null, location: null, + isActive: null, rowVersion: null, }, ], @@ -150,6 +152,7 @@ describe('Lease Surplus Declaration', () => { propertyId: 0, propertyName: null, location: null, + isActive: null, rowVersion: null, }, ], diff --git a/source/frontend/src/features/leases/list/LeaseSearchResults/LeaseProperties.tsx b/source/frontend/src/features/leases/list/LeaseSearchResults/LeaseProperties.tsx index e24bc475b0..824905209e 100644 --- a/source/frontend/src/features/leases/list/LeaseSearchResults/LeaseProperties.tsx +++ b/source/frontend/src/features/leases/list/LeaseSearchResults/LeaseProperties.tsx @@ -4,7 +4,7 @@ import styled from 'styled-components'; import { LinkButton } from '@/components/common/buttons'; import { ApiGen_Concepts_Property } from '@/models/api/generated/ApiGen_Concepts_Property'; -import { formatApiAddress } from '@/utils'; +import { formatApiAddress, pidFormatter } from '@/utils'; const PropertyRow = styled(Row)` border-radius: 0.4rem; @@ -40,7 +40,7 @@ const LeaseProperties: React.FunctionComponent< {property.pid && (
- PID: {property.pid} + PID: {pidFormatter(property?.pid?.toString())}
)} diff --git a/source/frontend/src/features/leases/shared/propertyPicker/LeasePropertySelector.tsx b/source/frontend/src/features/leases/shared/propertyPicker/LeasePropertySelector.tsx index 1da1365fd3..1c920124f5 100644 --- a/source/frontend/src/features/leases/shared/propertyPicker/LeasePropertySelector.tsx +++ b/source/frontend/src/features/leases/shared/propertyPicker/LeasePropertySelector.tsx @@ -1,13 +1,17 @@ import { FieldArray, FieldArrayRenderProps, FormikProps } from 'formik'; -import { LatLngLiteral } from 'leaflet'; +import { geoJSON, LatLngLiteral } from 'leaflet'; import isNumber from 'lodash/isNumber'; import { useCallback, useContext, useRef } from 'react'; import { Col, Row } from 'react-bootstrap'; +import { PiCornersOut } from 'react-icons/pi'; +import { LinkButton } from '@/components/common/buttons'; import { ModalProps } from '@/components/common/GenericModal'; import LoadingBackdrop from '@/components/common/LoadingBackdrop'; +import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; import { SelectedFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; import { Section } from '@/components/common/Section/Section'; +import TooltipWrapper from '@/components/common/TooltipWrapper'; import MapSelectorContainer from '@/components/propertySelector/MapSelectorContainer'; import { IMapProperty } from '@/components/propertySelector/models'; import { ModalContext } from '@/contexts/modalContext'; @@ -16,7 +20,13 @@ import { IPropertyFilter } from '@/features/properties/filter/IPropertyFilter'; import { useBcaAddress } from '@/features/properties/map/hooks/useBcaAddress'; import { useProperties } from '@/hooks/repositories/useProperties'; import { ApiGen_Concepts_PropertyView } from '@/models/api/generated/ApiGen_Concepts_PropertyView'; -import { exists, isLatLngInFeatureSetBoundary, isValidId, isValidString } from '@/utils'; +import { + exists, + isLatLngInFeatureSetBoundary, + isValidId, + isValidString, + latLngLiteralToGeometry, +} from '@/utils'; import { FormLeaseProperty, LeaseFormModel } from '../../models'; import SelectedPropertyHeaderRow from './selectedPropertyList/SelectedPropertyHeaderRow'; @@ -34,6 +44,8 @@ export const LeasePropertySelector: React.FunctionComponent(null); const addProperties = useCallback( @@ -79,6 +91,21 @@ export const LeasePropertySelector: React.FunctionComponent { + const fileProperties = values.properties; + + if (exists(fileProperties)) { + const locations = fileProperties + .map(p => p?.property?.polygon ?? latLngLiteralToGeometry(p?.property?.fileLocation)) + .filter(exists); + const bounds = geoJSON(locations).getBounds(); + + if (exists(bounds) && bounds.isValid()) { + mapMachine.requestFlyToBounds(bounds); + } + } + }; + const confirmAdd = useCallback( (propertiesToConfirm: FormLeaseProperty[]) => { setDisplayModal(false); @@ -234,7 +261,23 @@ export const LeasePropertySelector: React.FunctionComponent -
+
+ Selected Properties + + + + + + + + + } + > {formikProps.values.properties.map((leaseProperty, index) => { const property = leaseProperty?.property; diff --git a/source/frontend/src/features/leases/shared/propertyPicker/LeaseUpdatePropertySelector.tsx b/source/frontend/src/features/leases/shared/propertyPicker/LeaseUpdatePropertySelector.tsx index ecbe9a62f6..0f0bae2dda 100644 --- a/source/frontend/src/features/leases/shared/propertyPicker/LeaseUpdatePropertySelector.tsx +++ b/source/frontend/src/features/leases/shared/propertyPicker/LeaseUpdatePropertySelector.tsx @@ -1,16 +1,19 @@ import { AxiosError } from 'axios'; import { FieldArray, FieldArrayRenderProps, Formik, FormikProps } from 'formik'; -import { LatLngLiteral } from 'leaflet'; +import { geoJSON, LatLngLiteral } from 'leaflet'; import isNumber from 'lodash/isNumber'; import { useCallback, useContext, useRef, useState } from 'react'; import { Col, Row } from 'react-bootstrap'; +import { PiCornersOut } from 'react-icons/pi'; import { toast } from 'react-toastify'; +import { LinkButton } from '@/components/common/buttons'; import GenericModal, { ModalProps } from '@/components/common/GenericModal'; import LoadingBackdrop from '@/components/common/LoadingBackdrop'; import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; import { SelectedFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; import { Section } from '@/components/common/Section/Section'; +import TooltipWrapper from '@/components/common/TooltipWrapper'; import MapSelectorContainer from '@/components/propertySelector/MapSelectorContainer'; import { IMapProperty } from '@/components/propertySelector/models'; import { ModalContext } from '@/contexts/modalContext'; @@ -30,7 +33,13 @@ import { IApiError } from '@/interfaces/IApiError'; import { ApiGen_Concepts_Lease } from '@/models/api/generated/ApiGen_Concepts_Lease'; import { ApiGen_Concepts_PropertyView } from '@/models/api/generated/ApiGen_Concepts_PropertyView'; import { UserOverrideCode } from '@/models/api/UserOverrideCode'; -import { exists, isLatLngInFeatureSetBoundary, isValidId, isValidString } from '@/utils'; +import { + exists, + isLatLngInFeatureSetBoundary, + isValidId, + isValidString, + latLngLiteralToGeometry, +} from '@/utils'; import { useLeaseDetail } from '../../hooks/useLeaseDetail'; import { FormLeaseProperty, LeaseFormModel } from '../../models'; @@ -109,6 +118,20 @@ export const LeaseUpdatePropertySelector: React.FunctionComponent< const result = await getProperties.execute(params); return result?.items ?? undefined; }; + const fitBoundaries = () => { + const fileProperties = formikRef?.current?.values?.properties; + + if (exists(fileProperties)) { + const locations = fileProperties.map( + p => p?.property?.polygon ?? latLngLiteralToGeometry(p?.property?.fileLocation), + ); + const bounds = geoJSON(locations).getBounds(); + + if (exists(bounds) && bounds.isValid()) { + mapMachine.requestFlyToBounds(bounds); + } + } + }; const confirmAdd = useCallback( (propertiesToConfirm: FormLeaseProperty[]) => { @@ -365,7 +388,23 @@ export const LeaseUpdatePropertySelector: React.FunctionComponent< /> -
+
+ Selected Properties + + + + + + + + + } + > {formikProps.values.properties.map((leaseProperty, index) => { const property = leaseProperty?.property; diff --git a/source/frontend/src/features/leases/shared/propertyPicker/__snapshots__/LeasePropertySelector.test.tsx.snap b/source/frontend/src/features/leases/shared/propertyPicker/__snapshots__/LeasePropertySelector.test.tsx.snap index 060ff5bc3b..5367724fec 100644 --- a/source/frontend/src/features/leases/shared/propertyPicker/__snapshots__/LeasePropertySelector.test.tsx.snap +++ b/source/frontend/src/features/leases/shared/propertyPicker/__snapshots__/LeasePropertySelector.test.tsx.snap @@ -1068,7 +1068,43 @@ exports[`LeasePropertySelector component > renders as expected 1`] = `
- Selected properties +
+
+ Selected Properties +
+
+ +
+
@@ -1233,6 +1269,33 @@ exports[`LeasePropertySelector component > renders as expected 1`] = ` +
+ +
@@ -1513,6 +1576,33 @@ exports[`LeasePropertySelector component > renders as expected 1`] = `
+
+ +
diff --git a/source/frontend/src/features/leases/shared/propertyPicker/selectedPropertyList/SelectedPropertyRow.tsx b/source/frontend/src/features/leases/shared/propertyPicker/selectedPropertyList/SelectedPropertyRow.tsx index 094209c390..57510a726d 100644 --- a/source/frontend/src/features/leases/shared/propertyPicker/selectedPropertyList/SelectedPropertyRow.tsx +++ b/source/frontend/src/features/leases/shared/propertyPicker/selectedPropertyList/SelectedPropertyRow.tsx @@ -1,8 +1,11 @@ import { FormikProps, getIn } from 'formik'; +import { geoJSON } from 'leaflet'; +import { useCallback } from 'react'; import { Col, Row } from 'react-bootstrap'; +import { FaSearchPlus } from 'react-icons/fa'; import { RiDragMove2Line } from 'react-icons/ri'; -import { RemoveButton, StyledIconButton } from '@/components/common/buttons'; +import { LinkButton, RemoveButton, StyledIconButton } from '@/components/common/buttons'; import { InlineInput } from '@/components/common/form/styles'; import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; import { SelectedFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; @@ -10,6 +13,7 @@ import OverflowTip from '@/components/common/OverflowTip'; import AreaContainer from '@/components/measurements/AreaContainer'; import DraftCircleNumber from '@/components/propertySelector/selectedPropertyList/DraftCircleNumber'; import { FormLeaseProperty, LeaseFormModel } from '@/features/leases/models'; +import { exists } from '@/utils'; import { withNameSpace } from '@/utils/formUtils'; import { featuresetToMapProperty, getPropertyName, NameSourceType } from '@/utils/mapPropertyUtils'; @@ -53,6 +57,18 @@ export const SelectedPropertyRow: React.FunctionComponent { + const geom = property?.parcelFeature?.geometry ?? property?.pimsFeature?.geometry; + const bounds = geoJSON(geom).getBounds(); + + if (exists(bounds) && bounds.isValid()) { + mapMachine.requestFlyToBounds(bounds); + } + }, + [mapMachine], + ); + return ( <> @@ -82,6 +98,11 @@ export const SelectedPropertyRow: React.FunctionComponent + + onZoomToProperty(property)}> + + + diff --git a/source/frontend/src/features/leases/shared/propertyPicker/selectedPropertyList/__snapshots__/SelectedPropertyRow.test.tsx.snap b/source/frontend/src/features/leases/shared/propertyPicker/selectedPropertyList/__snapshots__/SelectedPropertyRow.test.tsx.snap index 0a23c96427..1b2f29fcdc 100644 --- a/source/frontend/src/features/leases/shared/propertyPicker/selectedPropertyList/__snapshots__/SelectedPropertyRow.test.tsx.snap +++ b/source/frontend/src/features/leases/shared/propertyPicker/selectedPropertyList/__snapshots__/SelectedPropertyRow.test.tsx.snap @@ -451,6 +451,33 @@ exports[`SelectedPropertyRow component > renders as expected 1`] = `
+
+ +
diff --git a/source/frontend/src/features/management/list/ManagementListView.test.tsx b/source/frontend/src/features/management/list/ManagementListView.test.tsx index fb1a1adc3f..39688bca47 100644 --- a/source/frontend/src/features/management/list/ManagementListView.test.tsx +++ b/source/frontend/src/features/management/list/ManagementListView.test.tsx @@ -126,6 +126,7 @@ describe('Management List View', () => { displayOrder: null, file: null, propertyName: null, + isActive: null, location: null, rowVersion: null, }, diff --git a/source/frontend/src/features/management/list/ManagementSearchResults/ManagementSearchResults.test.tsx b/source/frontend/src/features/management/list/ManagementSearchResults/ManagementSearchResults.test.tsx index b817a301a0..6b3400e1f6 100644 --- a/source/frontend/src/features/management/list/ManagementSearchResults/ManagementSearchResults.test.tsx +++ b/source/frontend/src/features/management/list/ManagementSearchResults/ManagementSearchResults.test.tsx @@ -86,6 +86,7 @@ describe('Management search results table', () => { file: null, propertyName: null, location: null, + isActive: null, rowVersion: null, }, { @@ -97,6 +98,7 @@ describe('Management search results table', () => { file: null, propertyName: null, location: null, + isActive: null, rowVersion: null, }, { @@ -108,6 +110,7 @@ describe('Management search results table', () => { file: null, propertyName: null, location: null, + isActive: null, rowVersion: null, }, ], diff --git a/source/frontend/src/features/mapSideBar/acquisition/AcquisitionContainer.tsx b/source/frontend/src/features/mapSideBar/acquisition/AcquisitionContainer.tsx index 262fa45134..500c31d8e1 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/AcquisitionContainer.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/AcquisitionContainer.tsx @@ -226,22 +226,16 @@ export const AcquisitionContainer: React.FunctionComponent fp.id === filePropertyId); - if (menuIndex < 0) { - return; - } - if (isEditing) { if (formikRef?.current?.dirty) { handleCancelClick(() => - pathGenerator.showFilePropertyIndex('acquisition', acquisitionFile.id, menuIndex + 1), + pathGenerator.showFilePropertyId('acquisition', acquisitionFile.id, filePropertyId), ); return; } } // The index needs to be offset to match the menu index - pathGenerator.showFilePropertyIndex('acquisition', acquisitionFile.id, menuIndex + 1); + pathGenerator.showFilePropertyId('acquisition', acquisitionFile.id, filePropertyId); }; const onEditProperties = () => { diff --git a/source/frontend/src/features/mapSideBar/acquisition/AcquisitionView.tsx b/source/frontend/src/features/mapSideBar/acquisition/AcquisitionView.tsx index ddd7a0d0a9..a136fb760c 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/AcquisitionView.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/AcquisitionView.tsx @@ -31,7 +31,7 @@ import { PropertyForm } from '../shared/models'; import SidebarFooter from '../shared/SidebarFooter'; import { StyledFormWrapper } from '../shared/styles'; import UpdateProperties from '../shared/update/properties/UpdateProperties'; -import { usePropertyIndexFromUrl } from '../shared/usePropertyIndexFromUrl'; +import { useFilePropertyIdFromUrl } from '../shared/usePropertyIndexFromUrl'; import { AcquisitionContainerState } from './AcquisitionContainer'; import { isAcquisitionFile } from './add/models'; import AcquisitionHeader from './common/AcquisitionHeader'; @@ -110,7 +110,7 @@ export const AcquisitionView: React.FunctionComponent = ( // Extract the zero-based property index from the current URL path. // It will be null if route is not matched - const currentPropertyIndex: number | null = usePropertyIndexFromUrl(); + const currentFilePropertyId: number | null = useFilePropertyIdFromUrl(); const statusSolver = new AcquisitionFileStatusUpdateSolver(acquisitionFile.fileStatusTypeCode); return ( @@ -167,7 +167,7 @@ export const AcquisitionView: React.FunctionComponent = ( {isAcquisitionFile(file) && ( = ( onSuccess={onSuccess} /> ( diff --git a/source/frontend/src/features/mapSideBar/acquisition/__snapshots__/AcquisitionView.test.tsx.snap b/source/frontend/src/features/mapSideBar/acquisition/__snapshots__/AcquisitionView.test.tsx.snap index 3a4f4565c5..58294219be 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/__snapshots__/AcquisitionView.test.tsx.snap +++ b/source/frontend/src/features/mapSideBar/acquisition/__snapshots__/AcquisitionView.test.tsx.snap @@ -991,11 +991,51 @@ exports[`AcquisitionView component > renders as expected 1`] = `
- - Properties - +
+ + Properties + +
+
+
+ +
+
renders as expected 1`] = `
+ Active
renders as expected 1`] = ` >
+
+ +
renders as expected 1`] = ` >
+
+ +
diff --git a/source/frontend/src/features/mapSideBar/acquisition/add/AcquisitionPropertiesSubForm.test.tsx b/source/frontend/src/features/mapSideBar/acquisition/add/AcquisitionPropertiesSubForm.test.tsx index 3455de35fa..5f93b8fffc 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/add/AcquisitionPropertiesSubForm.test.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/add/AcquisitionPropertiesSubForm.test.tsx @@ -76,8 +76,8 @@ describe('AcquisitionProperties component', () => { await waitFor(() => { expect(customSetFilePropertyLocations).toHaveBeenCalledWith([ - { location: { lat: 0, lng: 0 }, boundary: null }, - { location: { lat: 0, lng: 0 }, boundary: null }, + { location: { lat: undefined, lng: undefined }, boundary: null, isActive: true }, + { location: { lat: undefined, lng: undefined }, boundary: null, isActive: true }, ]); }); @@ -92,7 +92,7 @@ describe('AcquisitionProperties component', () => { await waitFor(() => { expect(customSetFilePropertyLocations).toHaveBeenCalledWith([ - { location: { lat: 0, lng: 0 }, boundary: null }, + { location: { lat: undefined, lng: undefined }, boundary: null, isActive: true }, ]); }); @@ -104,8 +104,8 @@ describe('AcquisitionProperties component', () => { await waitFor(() => { expect(customSetFilePropertyLocations).toHaveBeenCalledWith([ - { location: { lat: 0, lng: 0 }, boundary: null }, - { location: { lat: 0, lng: 0 }, boundary: null }, + { location: { lat: undefined, lng: undefined }, boundary: null, isActive: true }, + { location: { lat: undefined, lng: undefined }, boundary: null, isActive: true }, ]); }); @@ -121,8 +121,8 @@ describe('AcquisitionProperties component', () => { await waitFor(() => { expect(customSetFilePropertyLocations).toHaveBeenCalledWith([ - { location: { lat: 1, lng: 2 }, boundary: null }, - { location: { lat: 0, lng: 0 }, boundary: null }, + { location: { lat: 1, lng: 2 }, boundary: null, isActive: true }, + { location: { lat: undefined, lng: undefined }, boundary: null, isActive: true }, ]); }); }); @@ -138,8 +138,8 @@ describe('AcquisitionProperties component', () => { await waitFor(() => { expect(customSetFilePropertyLocations).toHaveBeenCalledWith([ - { location: { lat: 1, lng: 2 }, boundary: null }, - { location: { lat: 3, lng: 4 }, boundary: null }, + { location: { lat: 1, lng: 2 }, boundary: null, isActive: true }, + { location: { lat: 3, lng: 4 }, boundary: null, isActive: true }, ]); }); }); diff --git a/source/frontend/src/features/mapSideBar/acquisition/add/__snapshots__/AcquisitionPropertiesSubForm.test.tsx.snap b/source/frontend/src/features/mapSideBar/acquisition/add/__snapshots__/AcquisitionPropertiesSubForm.test.tsx.snap index a15673ae2e..2034fc677c 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/add/__snapshots__/AcquisitionPropertiesSubForm.test.tsx.snap +++ b/source/frontend/src/features/mapSideBar/acquisition/add/__snapshots__/AcquisitionPropertiesSubForm.test.tsx.snap @@ -894,7 +894,7 @@ exports[`AcquisitionProperties component > renders as expected 1`] = ` - .c7.btn { + .c8.btn { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -923,19 +923,19 @@ exports[`AcquisitionProperties component > renders as expected 1`] = ` cursor: pointer; } -.c7.btn .Button__value { +.c8.btn .Button__value { width: -webkit-max-content; width: -moz-max-content; width: max-content; } -.c7.btn:hover { +.c8.btn:hover { -webkit-text-decoration: underline; text-decoration: underline; opacity: 0.8; } -.c7.btn:focus { +.c8.btn:focus { outline-width: 2px; outline-style: solid; outline-color: #2E5DD7; @@ -943,31 +943,31 @@ exports[`AcquisitionProperties component > renders as expected 1`] = ` box-shadow: none; } -.c7.btn.btn-primary { +.c8.btn.btn-primary { color: #FFFFFF; background-color: #013366; } -.c7.btn.btn-primary:hover, -.c7.btn.btn-primary:active, -.c7.btn.btn-primary:focus { +.c8.btn.btn-primary:hover, +.c8.btn.btn-primary:active, +.c8.btn.btn-primary:focus { background-color: #1E5189; } -.c7.btn.btn-secondary { +.c8.btn.btn-secondary { color: #013366; background: none; border-color: #013366; } -.c7.btn.btn-secondary:hover, -.c7.btn.btn-secondary:active, -.c7.btn.btn-secondary:focus { +.c8.btn.btn-secondary:hover, +.c8.btn.btn-secondary:active, +.c8.btn.btn-secondary:focus { color: #FFFFFF; background-color: #013366; } -.c7.btn.btn-info { +.c8.btn.btn-info { color: #9F9D9C; border: none; background: none; @@ -975,66 +975,66 @@ exports[`AcquisitionProperties component > renders as expected 1`] = ` padding-right: 0.6rem; } -.c7.btn.btn-info:hover, -.c7.btn.btn-info:active, -.c7.btn.btn-info:focus { +.c8.btn.btn-info:hover, +.c8.btn.btn-info:active, +.c8.btn.btn-info:focus { color: var(--surface-color-primary-button-hover); background: none; } -.c7.btn.btn-light { +.c8.btn.btn-light { color: #FFFFFF; background-color: #606060; border: none; } -.c7.btn.btn-light:hover, -.c7.btn.btn-light:active, -.c7.btn.btn-light:focus { +.c8.btn.btn-light:hover, +.c8.btn.btn-light:active, +.c8.btn.btn-light:focus { color: #FFFFFF; background-color: #606060; } -.c7.btn.btn-dark { +.c8.btn.btn-dark { color: #FFFFFF; background-color: #474543; border: none; } -.c7.btn.btn-dark:hover, -.c7.btn.btn-dark:active, -.c7.btn.btn-dark:focus { +.c8.btn.btn-dark:hover, +.c8.btn.btn-dark:active, +.c8.btn.btn-dark:focus { color: #FFFFFF; background-color: #474543; } -.c7.btn.btn-danger { +.c8.btn.btn-danger { color: #FFFFFF; background-color: #CE3E39; } -.c7.btn.btn-danger:hover, -.c7.btn.btn-danger:active, -.c7.btn.btn-danger:focus { +.c8.btn.btn-danger:hover, +.c8.btn.btn-danger:active, +.c8.btn.btn-danger:focus { color: #FFFFFF; background-color: #CE3E39; } -.c7.btn.btn-warning { +.c8.btn.btn-warning { color: #FFFFFF; background-color: #FCBA19; border-color: #FCBA19; } -.c7.btn.btn-warning:hover, -.c7.btn.btn-warning:active, -.c7.btn.btn-warning:focus { +.c8.btn.btn-warning:hover, +.c8.btn.btn-warning:active, +.c8.btn.btn-warning:focus { color: #FFFFFF; border-color: #FCBA19; background-color: #FCBA19; } -.c7.btn.btn-link { +.c8.btn.btn-link { font-size: 1.6rem; font-weight: 400; color: var(--surface-color-primary-button-default); @@ -1058,9 +1058,9 @@ exports[`AcquisitionProperties component > renders as expected 1`] = ` text-decoration: underline; } -.c7.btn.btn-link:hover, -.c7.btn.btn-link:active, -.c7.btn.btn-link:focus { +.c8.btn.btn-link:hover, +.c8.btn.btn-link:active, +.c8.btn.btn-link:focus { color: var(--surface-color-primary-button-hover); -webkit-text-decoration: underline; text-decoration: underline; @@ -1070,15 +1070,15 @@ exports[`AcquisitionProperties component > renders as expected 1`] = ` outline: none; } -.c7.btn.btn-link:disabled, -.c7.btn.btn-link.disabled { +.c8.btn.btn-link:disabled, +.c8.btn.btn-link.disabled { color: #9F9D9C; background: none; pointer-events: none; } -.c7.btn:disabled, -.c7.btn:disabled:hover { +.c8.btn:disabled, +.c8.btn:disabled:hover { color: #9F9D9C; background-color: #EDEBE9; box-shadow: none; @@ -1090,66 +1090,66 @@ exports[`AcquisitionProperties component > renders as expected 1`] = ` cursor: not-allowed; } -.c7.Button .Button__icon { +.c8.Button .Button__icon { margin-right: 1.6rem; } -.c7.Button--icon-only:focus { +.c8.Button--icon-only:focus { outline: none; } -.c7.Button--icon-only .Button__icon { +.c8.Button--icon-only .Button__icon { margin-right: 0; } -.c8.c8.btn { +.c9.c9.btn { background-color: unset; border: none; } -.c8.c8.btn:hover, -.c8.c8.btn:focus, -.c8.c8.btn:active { +.c9.c9.btn:hover, +.c9.c9.btn:focus, +.c9.c9.btn:active { background-color: unset; outline: none; box-shadow: none; } -.c8.c8.btn svg { +.c9.c9.btn svg { -webkit-transition: all 0.3s ease-out; transition: all 0.3s ease-out; } -.c8.c8.btn svg:hover { +.c9.c9.btn svg:hover { -webkit-transition: all 0.3s ease-in; transition: all 0.3s ease-in; } -.c8.c8.btn.btn-primary svg { +.c9.c9.btn.btn-primary svg { color: #013366; } -.c8.c8.btn.btn-primary svg:hover { +.c9.c9.btn.btn-primary svg:hover { color: #013366; } -.c8.c8.btn.btn-light svg { +.c9.c9.btn.btn-light svg { color: var(--surface-color-primary-button-default); } -.c8.c8.btn.btn-light svg:hover { +.c9.c9.btn.btn-light svg:hover { color: #CE3E39; } -.c8.c8.btn.btn-info svg { +.c9.c9.btn.btn-info svg { color: var(--surface-color-primary-button-default); } -.c8.c8.btn.btn-info svg:hover { +.c9.c9.btn.btn-info svg:hover { color: var(--surface-color-primary-button-hover); } -.c9.c9.btn { +.c10.c10.btn { font-size: 1.4rem; color: #aaaaaa; -webkit-text-decoration: none; @@ -1157,13 +1157,13 @@ exports[`AcquisitionProperties component > renders as expected 1`] = ` line-height: unset; } -.c9.c9.btn .text { +.c10.c10.btn .text { display: none; } -.c9.c9.btn:hover, -.c9.c9.btn:active, -.c9.c9.btn:focus { +.c10.c10.btn:hover, +.c10.c10.btn:active, +.c10.c10.btn:focus { color: #d8292f; -webkit-text-decoration: none; text-decoration: none; @@ -1177,14 +1177,14 @@ exports[`AcquisitionProperties component > renders as expected 1`] = ` flex-direction: row; } -.c9.c9.btn:hover .text, -.c9.c9.btn:active .text, -.c9.c9.btn:focus .text { +.c10.c10.btn:hover .text, +.c10.c10.btn:active .text, +.c10.c10.btn:focus .text { display: inline; line-height: 2rem; } -.c6 { +.c7 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -1196,7 +1196,7 @@ exports[`AcquisitionProperties component > renders as expected 1`] = ` gap: 0.8rem; } -.c6 .form-label { +.c7 .form-label { -webkit-flex-shrink: 0; -ms-flex-negative: 0; flex-shrink: 0; @@ -1226,21 +1226,25 @@ exports[`AcquisitionProperties component > renders as expected 1`] = ` font-family: 'BcSans-Bold'; } -.c5 { +.c6 { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.c3 { +.c4 { min-width: 3rem; min-height: 4.5rem; } -.c4 { +.c5 { font-size: 1.2rem; } +.c3 { + min-height: 4.5rem; +} +
@@ -1295,7 +1299,7 @@ exports[`AcquisitionProperties component > renders as expected 1`] = `
renders as expected 1`] = ` class="mb-0 d-flex align-items-center" > renders as expected 1`] = ` renders as expected 1`] = `
PID: 123-456-789
@@ -1384,7 +1388,7 @@ exports[`AcquisitionProperties component > renders as expected 1`] = ` class="col-md-5" >
renders as expected 1`] = ` />
+
+ +
renders as expected 1`] = ` class="mb-0 d-flex align-items-center" > renders as expected 1`] = ` renders as expected 1`] = `
PIN: 1111222
@@ -1553,7 +1584,7 @@ exports[`AcquisitionProperties component > renders as expected 1`] = ` class="col-md-5" >
renders as expected 1`] = ` />
+
+ +
+
+
renders as expected 1`] = `
+ Active
renders as expected 1`] = ` >
+
+ +
diff --git a/source/frontend/src/features/mapSideBar/disposition/add/__snapshots__/AddDispositionContainerView.test.tsx.snap b/source/frontend/src/features/mapSideBar/disposition/add/__snapshots__/AddDispositionContainerView.test.tsx.snap index 4ad0ad59f1..ee7c6b0088 100644 --- a/source/frontend/src/features/mapSideBar/disposition/add/__snapshots__/AddDispositionContainerView.test.tsx.snap +++ b/source/frontend/src/features/mapSideBar/disposition/add/__snapshots__/AddDispositionContainerView.test.tsx.snap @@ -1497,7 +1497,43 @@ exports[`Add Disposition Container View > matches snapshot 1`] = `
- Selected properties +
+
+ Selected properties +
+
+ +
+
diff --git a/source/frontend/src/features/mapSideBar/disposition/common/DispositionHeader.tsx b/source/frontend/src/features/mapSideBar/disposition/common/DispositionHeader.tsx index 8638dee0c0..59470a229d 100644 --- a/source/frontend/src/features/mapSideBar/disposition/common/DispositionHeader.tsx +++ b/source/frontend/src/features/mapSideBar/disposition/common/DispositionHeader.tsx @@ -1,10 +1,11 @@ import React from 'react'; -import { Col } from 'react-bootstrap'; +import { Col, Row } from 'react-bootstrap'; +import styled from 'styled-components'; import AuditSection from '@/components/common/HeaderField/AuditSection'; import { HeaderField } from '@/components/common/HeaderField/HeaderField'; import StatusField from '@/components/common/HeaderField/StatusField'; -import { StyledFiller, StyledRow } from '@/components/common/HeaderField/styles'; +import { StyledFiller } from '@/components/common/HeaderField/styles'; import { Api_LastUpdatedBy } from '@/models/api/File'; import { ApiGen_Concepts_DispositionFile } from '@/models/api/generated/ApiGen_Concepts_DispositionFile'; import { exists } from '@/utils'; @@ -28,23 +29,40 @@ export const DispositionHeader: React.FunctionComponent< const propertyIds = dispositionFile?.fileProperties?.map(fp => fp.propertyId) ?? []; return ( - - - - D-{dispositionFile?.fileNumber} - - - - - - - {exists(dispositionFile?.fileStatusTypeCode) && ( - - )} - - - + + + + + D-{dispositionFile?.fileNumber} + + + + + + + + {exists(dispositionFile?.fileStatusTypeCode) && ( + + )} + + + + ); }; export default DispositionHeader; + +const Container = styled.div` + margin-top: 0.5rem; + margin-bottom: 1.5rem; + border-bottom-style: solid; + border-bottom-color: grey; + border-bottom-width: 0.1rem; + max-height: 25rem; + overflow-y: auto; + overflow-x: hidden; +`; diff --git a/source/frontend/src/features/mapSideBar/disposition/common/__snapshots__/DispositionHeader.test.tsx.snap b/source/frontend/src/features/mapSideBar/disposition/common/__snapshots__/DispositionHeader.test.tsx.snap index e00bf19934..85b002badf 100644 --- a/source/frontend/src/features/mapSideBar/disposition/common/__snapshots__/DispositionHeader.test.tsx.snap +++ b/source/frontend/src/features/mapSideBar/disposition/common/__snapshots__/DispositionHeader.test.tsx.snap @@ -28,14 +28,6 @@ exports[`DispositionHeader component > renders as expected when no data is provi white-space: nowrap; } -.c0 { - margin-top: 0.5rem; - margin-bottom: 1.5rem; - border-bottom-style: solid; - border-bottom-color: grey; - border-bottom-width: 0.1rem; -} - .c3 { height: 100%; display: -webkit-box; @@ -51,6 +43,17 @@ exports[`DispositionHeader component > renders as expected when no data is provi max-width: 60rem; } +.c0 { + margin-top: 0.5rem; + margin-bottom: 1.5rem; + border-bottom-style: solid; + border-bottom-color: grey; + border-bottom-width: 0.1rem; + max-height: 25rem; + overflow-y: auto; + overflow-x: hidden; +} + @media only screen and (max-width:1199px) { .c4 { text-align: left; @@ -58,100 +61,104 @@ exports[`DispositionHeader component > renders as expected when no data is provi }
- -
-
- D- + +
+
+ D- +
-
-
- + +
+
-
-
-
- - - Created: - - by - USER + Created: + by + + + USER + + - +
-
-
- - - Updated: - - by - USER + Updated: + by + + + USER + + - +
diff --git a/source/frontend/src/features/mapSideBar/disposition/form/DispositionPropertiesSubForm.test.tsx b/source/frontend/src/features/mapSideBar/disposition/form/DispositionPropertiesSubForm.test.tsx index 3ccbd38746..6439655bcc 100644 --- a/source/frontend/src/features/mapSideBar/disposition/form/DispositionPropertiesSubForm.test.tsx +++ b/source/frontend/src/features/mapSideBar/disposition/form/DispositionPropertiesSubForm.test.tsx @@ -75,8 +75,8 @@ describe('DispositionPropertiesSubForm component', () => { await waitFor(() => { expect(customSetFilePropertyLocations).toHaveBeenCalledWith([ - { location: { lat: 0, lng: 0 }, boundary: null }, - { location: { lat: 0, lng: 0 }, boundary: null }, + { location: { lat: undefined, lng: undefined }, boundary: null, isActive: true }, + { location: { lat: undefined, lng: undefined }, boundary: null, isActive: true }, ]); }); @@ -91,7 +91,7 @@ describe('DispositionPropertiesSubForm component', () => { await waitFor(() => { expect(customSetFilePropertyLocations).toHaveBeenCalledWith([ - { location: { lat: 0, lng: 0 }, boundary: null }, + { location: { lat: undefined, lng: undefined }, boundary: null, isActive: true }, ]); }); @@ -103,8 +103,8 @@ describe('DispositionPropertiesSubForm component', () => { await waitFor(() => { expect(customSetFilePropertyLocations).toHaveBeenCalledWith([ - { location: { lat: 0, lng: 0 }, boundary: null }, - { location: { lat: 0, lng: 0 }, boundary: null }, + { location: { lat: undefined, lng: undefined }, boundary: null, isActive: true }, + { location: { lat: undefined, lng: undefined }, boundary: null, isActive: true }, ]); }); @@ -120,8 +120,8 @@ describe('DispositionPropertiesSubForm component', () => { await waitFor(() => { expect(customSetFilePropertyLocations).toHaveBeenCalledWith([ - { location: { lat: 1, lng: 2 }, boundary: null }, - { location: { lat: 0, lng: 0 }, boundary: null }, + { location: { lat: 1, lng: 2 }, boundary: null, isActive: true }, + { location: { lat: undefined, lng: undefined }, boundary: null, isActive: true }, ]); }); }); @@ -137,8 +137,8 @@ describe('DispositionPropertiesSubForm component', () => { await waitFor(() => { expect(customSetFilePropertyLocations).toHaveBeenCalledWith([ - { location: { lat: 1, lng: 2 }, boundary: null }, - { location: { lat: 3, lng: 4 }, boundary: null }, + { location: { lat: 1, lng: 2 }, boundary: null, isActive: true }, + { location: { lat: 3, lng: 4 }, boundary: null, isActive: true }, ]); }); }); diff --git a/source/frontend/src/features/mapSideBar/disposition/form/DispositionPropertiesSubForm.tsx b/source/frontend/src/features/mapSideBar/disposition/form/DispositionPropertiesSubForm.tsx index 41bfcade8e..b73a41fb8b 100644 --- a/source/frontend/src/features/mapSideBar/disposition/form/DispositionPropertiesSubForm.tsx +++ b/source/frontend/src/features/mapSideBar/disposition/form/DispositionPropertiesSubForm.tsx @@ -1,17 +1,21 @@ import { FieldArray, FormikProps } from 'formik'; -import { LatLngLiteral } from 'leaflet'; +import { geoJSON, LatLngLiteral } from 'leaflet'; import isNumber from 'lodash/isNumber'; import { Col, Row } from 'react-bootstrap'; +import { PiCornersOut } from 'react-icons/pi'; +import { LinkButton } from '@/components/common/buttons/LinkButton'; import LoadingBackdrop from '@/components/common/LoadingBackdrop'; +import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; import { SelectedFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; import { Section } from '@/components/common/Section/Section'; +import TooltipWrapper from '@/components/common/TooltipWrapper'; import MapSelectorContainer from '@/components/propertySelector/MapSelectorContainer'; import SelectedPropertyHeaderRow from '@/components/propertySelector/selectedPropertyList/SelectedPropertyHeaderRow'; import SelectedPropertyRow from '@/components/propertySelector/selectedPropertyList/SelectedPropertyRow'; import { useBcaAddress } from '@/features/properties/map/hooks/useBcaAddress'; import { useModalContext } from '@/hooks/useModalContext'; -import { isLatLngInFeatureSetBoundary } from '@/utils'; +import { exists, isLatLngInFeatureSetBoundary, latLngLiteralToGeometry } from '@/utils'; import { AddressForm, PropertyForm } from '../../shared/models'; import { DispositionFormModel } from '../models/DispositionFormModel'; @@ -29,6 +33,21 @@ const DispositionPropertiesSubForm: React.FunctionComponent { + const fileProperties = formikProps.values.fileProperties; + + if (exists(fileProperties)) { + const locations = fileProperties.map( + p => p?.polygon ?? latLngLiteralToGeometry(p?.fileLocation), + ); + const bounds = geoJSON(locations).getBounds(); + + mapMachine.requestFlyToBounds(bounds); + } + }; + return ( <>
@@ -115,7 +134,23 @@ const DispositionPropertiesSubForm: React.FunctionComponent -
+
+ Selected properties + + + + + + + + + } + > {formikProps.values.fileProperties.map((property, index) => ( renders as expected 1`] = `
- .c7.btn { + .c2.btn { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -923,19 +923,19 @@ exports[`DispositionPropertiesSubForm component > renders as expected 1`] = ` cursor: pointer; } -.c7.btn .Button__value { +.c2.btn .Button__value { width: -webkit-max-content; width: -moz-max-content; width: max-content; } -.c7.btn:hover { +.c2.btn:hover { -webkit-text-decoration: underline; text-decoration: underline; opacity: 0.8; } -.c7.btn:focus { +.c2.btn:focus { outline-width: 2px; outline-style: solid; outline-color: #2E5DD7; @@ -943,31 +943,31 @@ exports[`DispositionPropertiesSubForm component > renders as expected 1`] = ` box-shadow: none; } -.c7.btn.btn-primary { +.c2.btn.btn-primary { color: #FFFFFF; background-color: #013366; } -.c7.btn.btn-primary:hover, -.c7.btn.btn-primary:active, -.c7.btn.btn-primary:focus { +.c2.btn.btn-primary:hover, +.c2.btn.btn-primary:active, +.c2.btn.btn-primary:focus { background-color: #1E5189; } -.c7.btn.btn-secondary { +.c2.btn.btn-secondary { color: #013366; background: none; border-color: #013366; } -.c7.btn.btn-secondary:hover, -.c7.btn.btn-secondary:active, -.c7.btn.btn-secondary:focus { +.c2.btn.btn-secondary:hover, +.c2.btn.btn-secondary:active, +.c2.btn.btn-secondary:focus { color: #FFFFFF; background-color: #013366; } -.c7.btn.btn-info { +.c2.btn.btn-info { color: #9F9D9C; border: none; background: none; @@ -975,66 +975,66 @@ exports[`DispositionPropertiesSubForm component > renders as expected 1`] = ` padding-right: 0.6rem; } -.c7.btn.btn-info:hover, -.c7.btn.btn-info:active, -.c7.btn.btn-info:focus { +.c2.btn.btn-info:hover, +.c2.btn.btn-info:active, +.c2.btn.btn-info:focus { color: var(--surface-color-primary-button-hover); background: none; } -.c7.btn.btn-light { +.c2.btn.btn-light { color: #FFFFFF; background-color: #606060; border: none; } -.c7.btn.btn-light:hover, -.c7.btn.btn-light:active, -.c7.btn.btn-light:focus { +.c2.btn.btn-light:hover, +.c2.btn.btn-light:active, +.c2.btn.btn-light:focus { color: #FFFFFF; background-color: #606060; } -.c7.btn.btn-dark { +.c2.btn.btn-dark { color: #FFFFFF; background-color: #474543; border: none; } -.c7.btn.btn-dark:hover, -.c7.btn.btn-dark:active, -.c7.btn.btn-dark:focus { +.c2.btn.btn-dark:hover, +.c2.btn.btn-dark:active, +.c2.btn.btn-dark:focus { color: #FFFFFF; background-color: #474543; } -.c7.btn.btn-danger { +.c2.btn.btn-danger { color: #FFFFFF; background-color: #CE3E39; } -.c7.btn.btn-danger:hover, -.c7.btn.btn-danger:active, -.c7.btn.btn-danger:focus { +.c2.btn.btn-danger:hover, +.c2.btn.btn-danger:active, +.c2.btn.btn-danger:focus { color: #FFFFFF; background-color: #CE3E39; } -.c7.btn.btn-warning { +.c2.btn.btn-warning { color: #FFFFFF; background-color: #FCBA19; border-color: #FCBA19; } -.c7.btn.btn-warning:hover, -.c7.btn.btn-warning:active, -.c7.btn.btn-warning:focus { +.c2.btn.btn-warning:hover, +.c2.btn.btn-warning:active, +.c2.btn.btn-warning:focus { color: #FFFFFF; border-color: #FCBA19; background-color: #FCBA19; } -.c7.btn.btn-link { +.c2.btn.btn-link { font-size: 1.6rem; font-weight: 400; color: var(--surface-color-primary-button-default); @@ -1058,9 +1058,9 @@ exports[`DispositionPropertiesSubForm component > renders as expected 1`] = ` text-decoration: underline; } -.c7.btn.btn-link:hover, -.c7.btn.btn-link:active, -.c7.btn.btn-link:focus { +.c2.btn.btn-link:hover, +.c2.btn.btn-link:active, +.c2.btn.btn-link:focus { color: var(--surface-color-primary-button-hover); -webkit-text-decoration: underline; text-decoration: underline; @@ -1070,15 +1070,15 @@ exports[`DispositionPropertiesSubForm component > renders as expected 1`] = ` outline: none; } -.c7.btn.btn-link:disabled, -.c7.btn.btn-link.disabled { +.c2.btn.btn-link:disabled, +.c2.btn.btn-link.disabled { color: #9F9D9C; background: none; pointer-events: none; } -.c7.btn:disabled, -.c7.btn:disabled:hover { +.c2.btn:disabled, +.c2.btn:disabled:hover { color: #9F9D9C; background-color: #EDEBE9; box-shadow: none; @@ -1090,66 +1090,66 @@ exports[`DispositionPropertiesSubForm component > renders as expected 1`] = ` cursor: not-allowed; } -.c7.Button .Button__icon { +.c2.Button .Button__icon { margin-right: 1.6rem; } -.c7.Button--icon-only:focus { +.c2.Button--icon-only:focus { outline: none; } -.c7.Button--icon-only .Button__icon { +.c2.Button--icon-only .Button__icon { margin-right: 0; } -.c8.c8.btn { +.c9.c9.btn { background-color: unset; border: none; } -.c8.c8.btn:hover, -.c8.c8.btn:focus, -.c8.c8.btn:active { +.c9.c9.btn:hover, +.c9.c9.btn:focus, +.c9.c9.btn:active { background-color: unset; outline: none; box-shadow: none; } -.c8.c8.btn svg { +.c9.c9.btn svg { -webkit-transition: all 0.3s ease-out; transition: all 0.3s ease-out; } -.c8.c8.btn svg:hover { +.c9.c9.btn svg:hover { -webkit-transition: all 0.3s ease-in; transition: all 0.3s ease-in; } -.c8.c8.btn.btn-primary svg { +.c9.c9.btn.btn-primary svg { color: #013366; } -.c8.c8.btn.btn-primary svg:hover { +.c9.c9.btn.btn-primary svg:hover { color: #013366; } -.c8.c8.btn.btn-light svg { +.c9.c9.btn.btn-light svg { color: var(--surface-color-primary-button-default); } -.c8.c8.btn.btn-light svg:hover { +.c9.c9.btn.btn-light svg:hover { color: #CE3E39; } -.c8.c8.btn.btn-info svg { +.c9.c9.btn.btn-info svg { color: var(--surface-color-primary-button-default); } -.c8.c8.btn.btn-info svg:hover { +.c9.c9.btn.btn-info svg:hover { color: var(--surface-color-primary-button-hover); } -.c9.c9.btn { +.c10.c10.btn { font-size: 1.4rem; color: #aaaaaa; -webkit-text-decoration: none; @@ -1157,13 +1157,13 @@ exports[`DispositionPropertiesSubForm component > renders as expected 1`] = ` line-height: unset; } -.c9.c9.btn .text { +.c10.c10.btn .text { display: none; } -.c9.c9.btn:hover, -.c9.c9.btn:active, -.c9.c9.btn:focus { +.c10.c10.btn:hover, +.c10.c10.btn:active, +.c10.c10.btn:focus { color: #d8292f; -webkit-text-decoration: none; text-decoration: none; @@ -1177,14 +1177,14 @@ exports[`DispositionPropertiesSubForm component > renders as expected 1`] = ` flex-direction: row; } -.c9.c9.btn:hover .text, -.c9.c9.btn:active .text, -.c9.c9.btn:focus .text { +.c10.c10.btn:hover .text, +.c10.c10.btn:active .text, +.c10.c10.btn:focus .text { display: inline; line-height: 2rem; } -.c6 { +.c8 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -1196,7 +1196,7 @@ exports[`DispositionPropertiesSubForm component > renders as expected 1`] = ` gap: 0.8rem; } -.c6 .form-label { +.c8 .form-label { -webkit-flex-shrink: 0; -ms-flex-negative: 0; flex-shrink: 0; @@ -1217,7 +1217,7 @@ exports[`DispositionPropertiesSubForm component > renders as expected 1`] = ` border-radius: 0.5rem; } -.c2 { +.c3 { font-size: 1.6rem; color: #9F9D9C; border-bottom: 0.2rem solid #606060; @@ -1226,21 +1226,25 @@ exports[`DispositionPropertiesSubForm component > renders as expected 1`] = ` font-family: 'BcSans-Bold'; } -.c5 { +.c7 { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.c3 { +.c5 { min-width: 3rem; min-height: 4.5rem; } -.c4 { +.c6 { font-size: 1.2rem; } +.c4 { + min-height: 4.5rem; +} +
@@ -1253,7 +1257,43 @@ exports[`DispositionPropertiesSubForm component > renders as expected 1`] = `
- Selected properties +
+
+ Selected properties +
+
+ +
+
@@ -1261,7 +1301,7 @@ exports[`DispositionPropertiesSubForm component > renders as expected 1`] = ` class="collapse show" >
renders as expected 1`] = `
renders as expected 1`] = ` class="mb-0 d-flex align-items-center" > renders as expected 1`] = ` renders as expected 1`] = `
PID: 123-456-789
@@ -1384,7 +1424,7 @@ exports[`DispositionPropertiesSubForm component > renders as expected 1`] = ` class="col-md-5" >
renders as expected 1`] = ` />
+
+ +
renders as expected 1`] = ` class="mb-0 d-flex align-items-center" > renders as expected 1`] = ` renders as expected 1`] = `
PIN: 1111222
@@ -1553,7 +1620,7 @@ exports[`DispositionPropertiesSubForm component > renders as expected 1`] = ` class="col-md-5" >
renders as expected 1`] = ` />
+
+ +
+
+
renders as expected 1`] = `
+ Active
renders as expected 1`] = ` >
+
+ +
diff --git a/source/frontend/src/features/mapSideBar/lease/detail/LeaseTabsContainer.tsx b/source/frontend/src/features/mapSideBar/lease/detail/LeaseTabsContainer.tsx index b8ed7373dd..74aaf887e7 100644 --- a/source/frontend/src/features/mapSideBar/lease/detail/LeaseTabsContainer.tsx +++ b/source/frontend/src/features/mapSideBar/lease/detail/LeaseTabsContainer.tsx @@ -230,7 +230,7 @@ export const LeaseTabsContainer: React.FC = ({ type={NoteTypes.Lease_File} entityId={lease?.id} onSuccess={onSuccess} - NoteListView={NoteListView} + View={NoteListView} /> ), key: LeaseFileTabNames.notes, diff --git a/source/frontend/src/features/mapSideBar/management/ManagementContainer.tsx b/source/frontend/src/features/mapSideBar/management/ManagementContainer.tsx index 0a5eaaf26f..4cdcffb10a 100644 --- a/source/frontend/src/features/mapSideBar/management/ManagementContainer.tsx +++ b/source/frontend/src/features/mapSideBar/management/ManagementContainer.tsx @@ -156,22 +156,16 @@ export const ManagementContainer: React.FunctionComponent fp.id === filePropertyId); - if (menuIndex < 0) { - return; - } - if (isEditing) { if (formikRef?.current?.dirty) { handleCancelClick(() => - pathGenerator.showFilePropertyIndex('management', managementFile.id, menuIndex + 1), + pathGenerator.showFilePropertyId('management', managementFile.id, filePropertyId), ); return; } } // The index needs to be offset to match the menu index - pathGenerator.showFilePropertyIndex('management', managementFile.id, menuIndex + 1); + pathGenerator.showFilePropertyId('management', managementFile.id, filePropertyId); }; const onEditProperties = () => { diff --git a/source/frontend/src/features/mapSideBar/management/ManagementView.test.tsx b/source/frontend/src/features/mapSideBar/management/ManagementView.test.tsx index de52958bc6..ac7e6a0fe4 100644 --- a/source/frontend/src/features/mapSideBar/management/ManagementView.test.tsx +++ b/source/frontend/src/features/mapSideBar/management/ManagementView.test.tsx @@ -87,7 +87,10 @@ vi.mocked(useApiProperties).mockReturnValue({ }); vi.mock('@/hooks/useLtsa'); -vi.mocked(useLtsa).mockReturnValue(getMockRepositoryObj()); +vi.mocked(useLtsa, { partial: true }).mockReturnValue({ + ltsaRequestWrapper: getMockRepositoryObj(), + getStrataPlanCommonProperty: getMockRepositoryObj(), +}); vi.mock('@/hooks/repositories/useProjectProvider'); vi.mocked(useProjectProvider, { partial: true }).mockReturnValue({ diff --git a/source/frontend/src/features/mapSideBar/management/ManagementView.tsx b/source/frontend/src/features/mapSideBar/management/ManagementView.tsx index a03fe19362..888b7658bb 100644 --- a/source/frontend/src/features/mapSideBar/management/ManagementView.tsx +++ b/source/frontend/src/features/mapSideBar/management/ManagementView.tsx @@ -31,7 +31,7 @@ import { PropertyForm } from '../shared/models'; import SidebarFooter from '../shared/SidebarFooter'; import { StyledFormWrapper } from '../shared/styles'; import UpdateProperties from '../shared/update/properties/UpdateProperties'; -import { usePropertyIndexFromUrl } from '../shared/usePropertyIndexFromUrl'; +import { useFilePropertyIdFromUrl } from '../shared/usePropertyIndexFromUrl'; import ManagementHeader from './common/ManagementHeader'; import ManagementRouter from './router/ManagementRouter'; import ManagementStatusUpdateSolver from './tabs/fileDetails/detail/ManagementStatusUpdateSolver'; @@ -77,8 +77,8 @@ export const ManagementView: React.FunctionComponent = ({ const location = useLocation(); const history = useHistory(); const match = useRouteMatch(); - const { hasClaim } = useKeycloakWrapper(); const { lastUpdatedBy } = useContext(SideBarContext); + const { hasClaim } = useKeycloakWrapper(); // match for property menu routes - eg /property/1/ltsa const fileMatch = matchPath>(location.pathname, `${match.path}/:tab`); @@ -102,7 +102,7 @@ export const ManagementView: React.FunctionComponent = ({ // Extract the zero-based property index from the current URL path. // It will be null if route is not matched - const currentPropertyIndex: number | null = usePropertyIndexFromUrl(); + const currentFilePropertyId: number | null = useFilePropertyIdFromUrl(); const statusSolver = new ManagementStatusUpdateSolver(managementFile); return ( @@ -117,6 +117,7 @@ export const ManagementView: React.FunctionComponent = ({ confirmBeforeAdd={confirmBeforeAdd} canRemove={canRemove} formikRef={formikRef} + disableProperties confirmBeforeAddMessage={ <>

This property has already been added to one or more management files.

@@ -150,7 +151,7 @@ export const ManagementView: React.FunctionComponent = ({ leftComponent={ = ({ onSuccess={onSuccess} /> ( diff --git a/source/frontend/src/features/mapSideBar/management/__snapshots__/ManagementView.test.tsx.snap b/source/frontend/src/features/mapSideBar/management/__snapshots__/ManagementView.test.tsx.snap index d67c25ab1b..cff159eefb 100644 --- a/source/frontend/src/features/mapSideBar/management/__snapshots__/ManagementView.test.tsx.snap +++ b/source/frontend/src/features/mapSideBar/management/__snapshots__/ManagementView.test.tsx.snap @@ -990,11 +990,51 @@ exports[`ManagementView component > renders as expected 1`] = `
- - Properties - +
+ + Properties + +
+
+
+ +
+
renders as expected 1`] = `
+ Active
renders as expected 1`] = ` >
+
+ +
diff --git a/source/frontend/src/features/mapSideBar/management/form/ManagementForm.tsx b/source/frontend/src/features/mapSideBar/management/form/ManagementForm.tsx index 7cbae194d1..fc1de1ca70 100644 --- a/source/frontend/src/features/mapSideBar/management/form/ManagementForm.tsx +++ b/source/frontend/src/features/mapSideBar/management/form/ManagementForm.tsx @@ -133,7 +133,7 @@ const ManagementForm: React.FC = props => {
- +
); diff --git a/source/frontend/src/features/mapSideBar/management/form/ManagementPropertiesSubForm.test.tsx b/source/frontend/src/features/mapSideBar/management/form/ManagementPropertiesSubForm.test.tsx index 7664d0d2fa..33faa81980 100644 --- a/source/frontend/src/features/mapSideBar/management/form/ManagementPropertiesSubForm.test.tsx +++ b/source/frontend/src/features/mapSideBar/management/form/ManagementPropertiesSubForm.test.tsx @@ -75,8 +75,8 @@ describe('ManagementPropertiesSubForm component', () => { await waitFor(() => { expect(customSetFilePropertyLocations).toHaveBeenCalledWith([ - { location: { lat: 0, lng: 0 }, boundary: null }, - { location: { lat: 0, lng: 0 }, boundary: null }, + { location: { lat: undefined, lng: undefined }, boundary: null, isActive: true }, + { location: { lat: undefined, lng: undefined }, boundary: null, isActive: true }, ]); }); @@ -91,7 +91,7 @@ describe('ManagementPropertiesSubForm component', () => { await waitFor(() => { expect(customSetFilePropertyLocations).toHaveBeenCalledWith([ - { location: { lat: 0, lng: 0 }, boundary: null }, + { location: { lat: undefined, lng: undefined }, boundary: null, isActive: true }, ]); }); @@ -103,8 +103,8 @@ describe('ManagementPropertiesSubForm component', () => { await waitFor(() => { expect(customSetFilePropertyLocations).toHaveBeenCalledWith([ - { location: { lat: 0, lng: 0 }, boundary: null }, - { location: { lat: 0, lng: 0 }, boundary: null }, + { location: { lat: undefined, lng: undefined }, boundary: null, isActive: true }, + { location: { lat: undefined, lng: undefined }, boundary: null, isActive: true }, ]); }); @@ -120,8 +120,8 @@ describe('ManagementPropertiesSubForm component', () => { await waitFor(() => { expect(customSetFilePropertyLocations).toHaveBeenCalledWith([ - { location: { lat: 1, lng: 2 }, boundary: null }, - { location: { lat: 0, lng: 0 }, boundary: null }, + { location: { lat: 1, lng: 2 }, boundary: null, isActive: true }, + { location: { lat: undefined, lng: undefined }, boundary: null, isActive: true }, ]); }); }); @@ -137,8 +137,8 @@ describe('ManagementPropertiesSubForm component', () => { await waitFor(() => { expect(customSetFilePropertyLocations).toHaveBeenCalledWith([ - { location: { lat: 1, lng: 2 }, boundary: null }, - { location: { lat: 3, lng: 4 }, boundary: null }, + { location: { lat: 1, lng: 2 }, boundary: null, isActive: true }, + { location: { lat: 3, lng: 4 }, boundary: null, isActive: true }, ]); }); }); diff --git a/source/frontend/src/features/mapSideBar/management/form/ManagementTeamSubForm.test.tsx b/source/frontend/src/features/mapSideBar/management/form/ManagementTeamSubForm.test.tsx index 25ad168ea9..c1022993e9 100644 --- a/source/frontend/src/features/mapSideBar/management/form/ManagementTeamSubForm.test.tsx +++ b/source/frontend/src/features/mapSideBar/management/form/ManagementTeamSubForm.test.tsx @@ -21,7 +21,7 @@ describe('ManagementTeamSubForm component', () => { const ref = createRef>(); const utils = render( - {formikProps => } + {formikProps => } , { ...renderOptions, diff --git a/source/frontend/src/features/mapSideBar/management/form/ManagementTeamSubForm.tsx b/source/frontend/src/features/mapSideBar/management/form/ManagementTeamSubForm.tsx index 5ee765acb9..2563efb9d9 100644 --- a/source/frontend/src/features/mapSideBar/management/form/ManagementTeamSubForm.tsx +++ b/source/frontend/src/features/mapSideBar/management/form/ManagementTeamSubForm.tsx @@ -18,7 +18,13 @@ import { WithManagementTeam, } from '../models/ManagementTeamSubFormModel'; -const ManagementTeamSubForm: React.FunctionComponent> = () => { +export interface IManagementTeamSubFormProps { + canEditDetails: boolean; +} + +const ManagementTeamSubForm: React.FunctionComponent = ({ + canEditDetails, +}) => { const { values, setFieldTouched, errors } = useFormikContext(); const { getOptionsByType } = useLookupCodeHelpers(); const { setModalContent, setDisplayModal } = useModalContext(); @@ -43,44 +49,52 @@ const ManagementTeamSubForm: React.FunctionComponent { setFieldTouched(`team.${index}.contact`); }} + disabled={!canEditDetails} /> + + - { - setModalContent({ - ...getDeleteModalProps(), - title: 'Remove Team Member', - message: 'Do you wish to remove this team member?', - okButtonText: 'Yes', - cancelButtonText: 'No', - handleOk: () => { - arrayHelpers.remove(index); - setDisplayModal(false); - }, - handleCancel: () => { - setDisplayModal(false); - }, - }); - setDisplayModal(true); - }} - /> + {canEditDetails && ( + { + setModalContent({ + ...getDeleteModalProps(), + title: 'Remove Team Member', + message: 'Do you wish to remove this team member?', + okButtonText: 'Yes', + cancelButtonText: 'No', + handleOk: () => { + arrayHelpers.remove(index); + setDisplayModal(false); + }, + handleCancel: () => { + setDisplayModal(false); + }, + }); + setDisplayModal(true); + }} + /> + )} + {isValidId(teamMember.contact?.organizationId) && !isValidId(teamMember.contact?.personId) && ( )} @@ -93,15 +107,17 @@ const ManagementTeamSubForm: React.FunctionComponent )} - { - const member = new ManagementTeamSubFormModel(); - arrayHelpers.push(member); - }} - > - + Add another team member - + {canEditDetails && ( + { + const member = new ManagementTeamSubFormModel(); + arrayHelpers.push(member); + }} + > + + Add another team member + + )} )} /> diff --git a/source/frontend/src/features/mapSideBar/management/form/__snapshots__/ManagementPropertiesSubForm.test.tsx.snap b/source/frontend/src/features/mapSideBar/management/form/__snapshots__/ManagementPropertiesSubForm.test.tsx.snap index 7999758fbc..24001c2e33 100644 --- a/source/frontend/src/features/mapSideBar/management/form/__snapshots__/ManagementPropertiesSubForm.test.tsx.snap +++ b/source/frontend/src/features/mapSideBar/management/form/__snapshots__/ManagementPropertiesSubForm.test.tsx.snap @@ -894,7 +894,7 @@ exports[`ManagementPropertiesSubForm component > renders as expected 1`] = ` - .c7.btn { + .c8.btn { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -923,19 +923,19 @@ exports[`ManagementPropertiesSubForm component > renders as expected 1`] = ` cursor: pointer; } -.c7.btn .Button__value { +.c8.btn .Button__value { width: -webkit-max-content; width: -moz-max-content; width: max-content; } -.c7.btn:hover { +.c8.btn:hover { -webkit-text-decoration: underline; text-decoration: underline; opacity: 0.8; } -.c7.btn:focus { +.c8.btn:focus { outline-width: 2px; outline-style: solid; outline-color: #2E5DD7; @@ -943,31 +943,31 @@ exports[`ManagementPropertiesSubForm component > renders as expected 1`] = ` box-shadow: none; } -.c7.btn.btn-primary { +.c8.btn.btn-primary { color: #FFFFFF; background-color: #013366; } -.c7.btn.btn-primary:hover, -.c7.btn.btn-primary:active, -.c7.btn.btn-primary:focus { +.c8.btn.btn-primary:hover, +.c8.btn.btn-primary:active, +.c8.btn.btn-primary:focus { background-color: #1E5189; } -.c7.btn.btn-secondary { +.c8.btn.btn-secondary { color: #013366; background: none; border-color: #013366; } -.c7.btn.btn-secondary:hover, -.c7.btn.btn-secondary:active, -.c7.btn.btn-secondary:focus { +.c8.btn.btn-secondary:hover, +.c8.btn.btn-secondary:active, +.c8.btn.btn-secondary:focus { color: #FFFFFF; background-color: #013366; } -.c7.btn.btn-info { +.c8.btn.btn-info { color: #9F9D9C; border: none; background: none; @@ -975,66 +975,66 @@ exports[`ManagementPropertiesSubForm component > renders as expected 1`] = ` padding-right: 0.6rem; } -.c7.btn.btn-info:hover, -.c7.btn.btn-info:active, -.c7.btn.btn-info:focus { +.c8.btn.btn-info:hover, +.c8.btn.btn-info:active, +.c8.btn.btn-info:focus { color: var(--surface-color-primary-button-hover); background: none; } -.c7.btn.btn-light { +.c8.btn.btn-light { color: #FFFFFF; background-color: #606060; border: none; } -.c7.btn.btn-light:hover, -.c7.btn.btn-light:active, -.c7.btn.btn-light:focus { +.c8.btn.btn-light:hover, +.c8.btn.btn-light:active, +.c8.btn.btn-light:focus { color: #FFFFFF; background-color: #606060; } -.c7.btn.btn-dark { +.c8.btn.btn-dark { color: #FFFFFF; background-color: #474543; border: none; } -.c7.btn.btn-dark:hover, -.c7.btn.btn-dark:active, -.c7.btn.btn-dark:focus { +.c8.btn.btn-dark:hover, +.c8.btn.btn-dark:active, +.c8.btn.btn-dark:focus { color: #FFFFFF; background-color: #474543; } -.c7.btn.btn-danger { +.c8.btn.btn-danger { color: #FFFFFF; background-color: #CE3E39; } -.c7.btn.btn-danger:hover, -.c7.btn.btn-danger:active, -.c7.btn.btn-danger:focus { +.c8.btn.btn-danger:hover, +.c8.btn.btn-danger:active, +.c8.btn.btn-danger:focus { color: #FFFFFF; background-color: #CE3E39; } -.c7.btn.btn-warning { +.c8.btn.btn-warning { color: #FFFFFF; background-color: #FCBA19; border-color: #FCBA19; } -.c7.btn.btn-warning:hover, -.c7.btn.btn-warning:active, -.c7.btn.btn-warning:focus { +.c8.btn.btn-warning:hover, +.c8.btn.btn-warning:active, +.c8.btn.btn-warning:focus { color: #FFFFFF; border-color: #FCBA19; background-color: #FCBA19; } -.c7.btn.btn-link { +.c8.btn.btn-link { font-size: 1.6rem; font-weight: 400; color: var(--surface-color-primary-button-default); @@ -1058,9 +1058,9 @@ exports[`ManagementPropertiesSubForm component > renders as expected 1`] = ` text-decoration: underline; } -.c7.btn.btn-link:hover, -.c7.btn.btn-link:active, -.c7.btn.btn-link:focus { +.c8.btn.btn-link:hover, +.c8.btn.btn-link:active, +.c8.btn.btn-link:focus { color: var(--surface-color-primary-button-hover); -webkit-text-decoration: underline; text-decoration: underline; @@ -1070,15 +1070,15 @@ exports[`ManagementPropertiesSubForm component > renders as expected 1`] = ` outline: none; } -.c7.btn.btn-link:disabled, -.c7.btn.btn-link.disabled { +.c8.btn.btn-link:disabled, +.c8.btn.btn-link.disabled { color: #9F9D9C; background: none; pointer-events: none; } -.c7.btn:disabled, -.c7.btn:disabled:hover { +.c8.btn:disabled, +.c8.btn:disabled:hover { color: #9F9D9C; background-color: #EDEBE9; box-shadow: none; @@ -1090,66 +1090,66 @@ exports[`ManagementPropertiesSubForm component > renders as expected 1`] = ` cursor: not-allowed; } -.c7.Button .Button__icon { +.c8.Button .Button__icon { margin-right: 1.6rem; } -.c7.Button--icon-only:focus { +.c8.Button--icon-only:focus { outline: none; } -.c7.Button--icon-only .Button__icon { +.c8.Button--icon-only .Button__icon { margin-right: 0; } -.c8.c8.btn { +.c9.c9.btn { background-color: unset; border: none; } -.c8.c8.btn:hover, -.c8.c8.btn:focus, -.c8.c8.btn:active { +.c9.c9.btn:hover, +.c9.c9.btn:focus, +.c9.c9.btn:active { background-color: unset; outline: none; box-shadow: none; } -.c8.c8.btn svg { +.c9.c9.btn svg { -webkit-transition: all 0.3s ease-out; transition: all 0.3s ease-out; } -.c8.c8.btn svg:hover { +.c9.c9.btn svg:hover { -webkit-transition: all 0.3s ease-in; transition: all 0.3s ease-in; } -.c8.c8.btn.btn-primary svg { +.c9.c9.btn.btn-primary svg { color: #013366; } -.c8.c8.btn.btn-primary svg:hover { +.c9.c9.btn.btn-primary svg:hover { color: #013366; } -.c8.c8.btn.btn-light svg { +.c9.c9.btn.btn-light svg { color: var(--surface-color-primary-button-default); } -.c8.c8.btn.btn-light svg:hover { +.c9.c9.btn.btn-light svg:hover { color: #CE3E39; } -.c8.c8.btn.btn-info svg { +.c9.c9.btn.btn-info svg { color: var(--surface-color-primary-button-default); } -.c8.c8.btn.btn-info svg:hover { +.c9.c9.btn.btn-info svg:hover { color: var(--surface-color-primary-button-hover); } -.c9.c9.btn { +.c10.c10.btn { font-size: 1.4rem; color: #aaaaaa; -webkit-text-decoration: none; @@ -1157,13 +1157,13 @@ exports[`ManagementPropertiesSubForm component > renders as expected 1`] = ` line-height: unset; } -.c9.c9.btn .text { +.c10.c10.btn .text { display: none; } -.c9.c9.btn:hover, -.c9.c9.btn:active, -.c9.c9.btn:focus { +.c10.c10.btn:hover, +.c10.c10.btn:active, +.c10.c10.btn:focus { color: #d8292f; -webkit-text-decoration: none; text-decoration: none; @@ -1177,14 +1177,14 @@ exports[`ManagementPropertiesSubForm component > renders as expected 1`] = ` flex-direction: row; } -.c9.c9.btn:hover .text, -.c9.c9.btn:active .text, -.c9.c9.btn:focus .text { +.c10.c10.btn:hover .text, +.c10.c10.btn:active .text, +.c10.c10.btn:focus .text { display: inline; line-height: 2rem; } -.c6 { +.c7 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -1196,7 +1196,7 @@ exports[`ManagementPropertiesSubForm component > renders as expected 1`] = ` gap: 0.8rem; } -.c6 .form-label { +.c7 .form-label { -webkit-flex-shrink: 0; -ms-flex-negative: 0; flex-shrink: 0; @@ -1226,21 +1226,25 @@ exports[`ManagementPropertiesSubForm component > renders as expected 1`] = ` font-family: 'BcSans-Bold'; } -.c5 { +.c6 { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.c3 { +.c4 { min-width: 3rem; min-height: 4.5rem; } -.c4 { +.c5 { font-size: 1.2rem; } +.c3 { + min-height: 4.5rem; +} +
@@ -1295,7 +1299,7 @@ exports[`ManagementPropertiesSubForm component > renders as expected 1`] = `
renders as expected 1`] = ` class="mb-0 d-flex align-items-center" > renders as expected 1`] = ` renders as expected 1`] = `
PID: 123-456-789
@@ -1384,7 +1388,7 @@ exports[`ManagementPropertiesSubForm component > renders as expected 1`] = ` class="col-md-5" >
renders as expected 1`] = ` />
+
+ +
renders as expected 1`] = ` class="mb-0 d-flex align-items-center" > renders as expected 1`] = ` renders as expected 1`] = `
PIN: 1111222
@@ -1553,7 +1584,7 @@ exports[`ManagementPropertiesSubForm component > renders as expected 1`] = ` class="col-md-5" >
renders as expected 1`] = ` />
+
+ +
- +
diff --git a/source/frontend/src/features/mapSideBar/project/tabs/ProjectTabsContainer.tsx b/source/frontend/src/features/mapSideBar/project/tabs/ProjectTabsContainer.tsx index 1c881e3bcd..fdfdb87621 100644 --- a/source/frontend/src/features/mapSideBar/project/tabs/ProjectTabsContainer.tsx +++ b/source/frontend/src/features/mapSideBar/project/tabs/ProjectTabsContainer.tsx @@ -68,11 +68,7 @@ const ProjectTabsContainer: React.FC = ({ if (project?.id && hasClaim(Claims.NOTE_VIEW)) { tabViews.push({ content: ( - + ), key: ProjectTabNames.notes, name: 'Notes', diff --git a/source/frontend/src/features/mapSideBar/property/ComposedProperty.ts b/source/frontend/src/features/mapSideBar/property/ComposedProperty.ts index 7318695b03..6cda966103 100644 --- a/source/frontend/src/features/mapSideBar/property/ComposedProperty.ts +++ b/source/frontend/src/features/mapSideBar/property/ComposedProperty.ts @@ -1,6 +1,6 @@ import { Feature, FeatureCollection, GeoJsonProperties, Geometry } from 'geojson'; -import { LtsaOrders } from '@/interfaces/ltsaModels'; +import { LtsaOrders, SpcpOrder } from '@/interfaces/ltsaModels'; import { ApiGen_Concepts_Property } from '@/models/api/generated/ApiGen_Concepts_Property'; import { ApiGen_Concepts_PropertyAssociations } from '@/models/api/generated/ApiGen_Concepts_PropertyAssociations'; import { IBcAssessmentSummary } from '@/models/layers/bcAssesment'; @@ -9,8 +9,10 @@ import { TANTALIS_CrownLandTenures_Feature_Properties } from '@/models/layers/cr export interface ComposedProperty { pid: string | undefined; pin: string | undefined; + planNumber: string | undefined; id: number | undefined; ltsaOrders: LtsaOrders | undefined; + spcpOrder: SpcpOrder | undefined; pimsProperty: ApiGen_Concepts_Property | undefined; propertyAssociations: ApiGen_Concepts_PropertyAssociations | undefined; parcelMapFeatureCollection: FeatureCollection | undefined; // TODO: These need to be strongly typed diff --git a/source/frontend/src/features/mapSideBar/property/InventoryTabs.tsx b/source/frontend/src/features/mapSideBar/property/InventoryTabs.tsx index 9e3aa5d9e4..deb5139149 100644 --- a/source/frontend/src/features/mapSideBar/property/InventoryTabs.tsx +++ b/source/frontend/src/features/mapSideBar/property/InventoryTabs.tsx @@ -21,6 +21,7 @@ export enum InventoryTabNames { title = 'ltsa', value = 'bcassessment', research = 'research', + plan = 'plan', pims = 'pims', takes = 'takes', management = 'management', @@ -43,6 +44,7 @@ export const InventoryTabs: React.FunctionComponent< menuIndex: string; id: string; researchId: string; + filePropertyId: string; }>(); return ( const leaseAssociations = composedPropertyState?.propertyAssociationWrapper?.response?.leaseAssociations; + useMemo( () => hasClaim(Claims.LEASE_VIEW) @@ -124,6 +126,10 @@ export const PropertyContainer: React.FunctionComponent composedPropertyState?.pin?.toString() ?? composedPropertyState?.apiWrapper?.response?.pin?.toString(); + const retrievedPlanNumber = + composedPropertyState?.planNumber?.toString() ?? + composedPropertyState?.apiWrapper?.response?.planNumber?.toString(); + const tabViews: TabInventoryView[] = []; let defaultTab = InventoryTabNames.title; @@ -140,6 +146,21 @@ export const PropertyContainer: React.FunctionComponent name: 'Title', }); + if (exists(retrievedPlanNumber) && isPlanNumberSPCP(retrievedPlanNumber)) { + tabViews.push({ + content: ( + + ), + key: InventoryTabNames.plan, + name: 'Plan', + }); + } + if (exists(composedPropertyState.composedProperty?.crownTenureFeatures)) { tabViews.push({ content: ( @@ -247,7 +268,7 @@ export const PropertyContainer: React.FunctionComponent type={NoteTypes.Property} entityId={composedPropertyState.apiWrapper.response.id} onSuccess={onChildSuccess} - NoteListView={NoteListView} + View={NoteListView} /> { + const setup = ( + renderOptions: RenderOptions & ILtsaPlanTabViewProps & { lease?: LeaseFormModel } = { + loading: false, + }, + ) => { + // render component under test + const component = render( + , + { + ...renderOptions, + history, + }, + ); + + return { + component, + }; + }; + + beforeAll(() => { + vi.useFakeTimers(); + vi.setSystemTime(new Date('06-Apr-2022 11:00 AM').getTime()); + }); + + afterAll(() => { + vi.useRealTimers(); + }); + + it('renders a spinner when the ltsa data is loading', () => { + const { + component: { getByTestId }, + } = setup({ loading: true }); + + const spinner = getByTestId('filter-backdrop-loading'); + expect(spinner).toBeVisible(); + }); + + it('renders as expected when provided valid ltsa data object and requested on datetime', () => { + const { component } = setup({ + spcpData: getMockLtsaSPCPResponse(), + ltsaRequestedOn: new Date('06-Apr-2022 11:00 AM GMT'), + loading: false, + }); + + expect(component.asFragment()).toMatchSnapshot(); + }); + + it('does not throw an exception for an invalid ltsa data object', () => { + const { + component: { getByText }, + } = setup({ + spcpData: {} as SpcpOrder, + ltsaRequestedOn: new Date(), + loading: false, + planNumber: 'VISXXXX', + }); + expect(getByText('Strata Plan Common Property Details')).toBeVisible(); + }); +}); diff --git a/source/frontend/src/features/mapSideBar/property/tabs/ltsa/LtsaPlanTabView.tsx b/source/frontend/src/features/mapSideBar/property/tabs/ltsa/LtsaPlanTabView.tsx new file mode 100644 index 0000000000..9ffc9efffb --- /dev/null +++ b/source/frontend/src/features/mapSideBar/property/tabs/ltsa/LtsaPlanTabView.tsx @@ -0,0 +1,229 @@ +import { FieldArray, Form, Formik, getIn } from 'formik'; +import noop from 'lodash/noop'; +import moment from 'moment'; +import { Fragment } from 'react'; +import { Col, Row } from 'react-bootstrap'; +import styled from 'styled-components'; + +import { Input } from '@/components/common/form'; +import { FormSection } from '@/components/common/form/styles'; +import LoadingBackdrop from '@/components/common/LoadingBackdrop'; +import { Section } from '@/components/common/Section/Section'; +import { SectionField } from '@/components/common/Section/SectionField'; +import { + InlineMessage, + StyledInlineMessageSection, +} from '@/components/common/Section/SectionStyles'; +import { + ChargesOnStrataCommonProperty, + LegalNotationsOnStrataCommonProperty, + OrderParent, + SpcpOrder, +} from '@/interfaces/ltsaModels'; +import { exists } from '@/utils'; +import { prettyFormatDate } from '@/utils/dateUtils'; +import { withNameSpace } from '@/utils/formUtils'; + +import LtsaChargeOwnerSubForm from './LtsaChargeOwnerSubForm'; + +export interface ILtsaPlanTabViewProps { + spcpData?: SpcpOrder; + ltsaRequestedOn?: Date; + loading: boolean; + planNumber?: string; +} + +export const LtsaPlanTabView: React.FunctionComponent< + React.PropsWithChildren +> = ({ spcpData, ltsaRequestedOn, loading, planNumber }) => { + const titleNameSpace = 'orderedProduct.fieldedData'; + + const charges: ChargesOnStrataCommonProperty[] = + getIn(spcpData, withNameSpace(titleNameSpace, 'chargesOnSCP')) ?? []; + const legalNotations: LegalNotationsOnStrataCommonProperty[] = + getIn(spcpData, withNameSpace(titleNameSpace, 'legalNotationsOnSCP')) ?? []; + + return ( + <> + + + {!loading && exists(planNumber) && !spcpData ? ( + + + Failed to load data from LTSA. +
+
Refresh this page to try again, or select a different property. If this error + persists, contact an administrator. +
+
+ ) : ( + + + {exists(ltsaRequestedOn) && ( + + + This data was retrieved from LTSA on{' '} + {moment(ltsaRequestedOn).format('DD-MMM-YYYY h:mm A')} + + + )} + +
+ + + + + + +
+ +
+
+ {legalNotations.length === 0 && 'None'} + + ( + + {legalNotations.map( + (notation: LegalNotationsOnStrataCommonProperty, index: number) => { + const innerNameSpace = withNameSpace( + titleNameSpace, + `legalNotationsOnSCP.${index}`, + ); + + return ( + + + + + {notation.legalNotationNumber} + + + + + {notation.status} + + + + + {prettyFormatDate( + notation.legalNotation.applicationReceivedDate, + )} + + + + +

{notation.legalNotation.legalNotationText}

+
+ + {index < legalNotations.length - 1 &&
} +
+ ); + }, + )} +
+ )} + /> +
+
+ +
+
+ {charges.length === 0 && 'None'} + + ( + + {charges.map((charge: ChargesOnStrataCommonProperty, index: number) => { + const innerNameSpace = withNameSpace( + titleNameSpace, + `chargesOnSCP.${index}`, + ); + return ( + + + {charge.chargeNumber} + + + {charge.charge.transactionType} + + + {prettyFormatDate(charge.charge.applicationReceivedDate)} + + + + + {index < charges.length - 1 &&
} +
+ ); + })} +
+ )} + /> +
+
+
+
+ )} + + ); +}; + +export const StyledForm = styled(Form)` + position: relative; + &&& { + input, + select, + textarea { + background: none; + border: none; + resize: none; + height: fit-content; + padding: 0; + } + .form-label { + font-weight: bold; + } + } +`; + +const defaultLtsaSPCPData: SpcpOrder = { + fileReference: 'folio', + productOrderParameters: { + strataPlanNumber: '', + includeCancelledInfo: false, + }, + orderId: '', + status: OrderParent.StatusEnum.Processing, + orderedProduct: { + fieldedData: { + strataPlanIdentifier: { + strataPlanNumber: '', + landTitleDistrict: '', + }, + legalNotationsOnSCP: [], + chargesOnSCP: [], + }, + }, + productType: OrderParent.ProductTypeEnum.CommonProperty, +}; + +export default LtsaPlanTabView; diff --git a/source/frontend/src/features/mapSideBar/property/tabs/ltsa/__snapshots__/LtsaPlanTabView.test.tsx.snap b/source/frontend/src/features/mapSideBar/property/tabs/ltsa/__snapshots__/LtsaPlanTabView.test.tsx.snap new file mode 100644 index 0000000000..4e93048a7b --- /dev/null +++ b/source/frontend/src/features/mapSideBar/property/tabs/ltsa/__snapshots__/LtsaPlanTabView.test.tsx.snap @@ -0,0 +1,401 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`LtsaPlanTabView component > renders as expected when provided valid ltsa data object and requested on datetime 1`] = ` + +
+
+ .c1 { + margin: 1.5rem; + padding: 0.5rem 1.5rem; + background-color: white; + text-align: left; +} + +.c2 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-wrap: nowrap; + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + gap: 0.8rem; + font-style: italic; +} + +.c7 { + float: right; + cursor: pointer; + font-size: 3.2rem; +} + +.c4 { + font-weight: bold; + color: var(--theme-blue-100); + border-bottom: 0.2rem var(--theme-blue-90) solid; + margin-bottom: 2.4rem; +} + +.c3 { + margin: 1.6rem; + padding: 1.6rem; + background-color: white; + text-align: left; + border-radius: 0.5rem; +} + +.c6.required::before { + content: '*'; + position: absolute; + top: 0.75rem; + left: 0rem; +} + +.c5 { + font-weight: bold; +} + +.c0 { + position: relative; +} + +.c0.c0.c0 input, +.c0.c0.c0 select, +.c0.c0.c0 textarea { + background: none; + border: none; + resize: none; + height: -webkit-fit-content; + height: -moz-fit-content; + height: fit-content; + padding: 0; +} + +.c0.c0.c0 .form-label { + font-weight: bold; +} + +
+
+
+ This data was retrieved from LTSA on 06-Apr-2022 4:00 AM +
+
+
+

+
+
+ Strata Plan Common Property Details +
+
+

+
+
+
+ +
+
+
+ +
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+

+
+
+ Legal Notations on Strata Common Property +
+
+

+
+
+
+
+
+
+ +
+
+ EP96596 +
+
+
+
+
+
+ +
+
+ ACTIVE +
+
+
+
+
+
+ +
+
+ Nov 14, 2000 +
+
+
+
+
+
+ +
+
+

+ THIS TITLE MAY BE AFFECTED BY A PERMIT UNDER PART 26 OF THE LOCAL +GOVERNMENT ACT, SEE EP96596 +(AS TO ALL EXCEPT CLOSED ROAD ON PLAN VIP80164) + +

+
+
+
+
+
+
+

+
+
+ Charges on Strata Common Property +
+
+ + + collapse-section + + + + +
+
+

+
+
+
+
+ +
+
+ EC43157 +
+
+
+
+ +
+
+ UNDERSURFACE AND OTHER EXC & RES +
+
+
+
+ +
+
+ May 8, 1989 +
+
+
+
+
+
+ +`; diff --git a/source/frontend/src/features/mapSideBar/property/tabs/propertyAssociations/AssociationHeader.tsx b/source/frontend/src/features/mapSideBar/property/tabs/propertyAssociations/AssociationHeader.tsx index b965feecd6..2c7c73e2bf 100644 --- a/source/frontend/src/features/mapSideBar/property/tabs/propertyAssociations/AssociationHeader.tsx +++ b/source/frontend/src/features/mapSideBar/property/tabs/propertyAssociations/AssociationHeader.tsx @@ -1,6 +1,8 @@ import { Col, Row } from 'react-bootstrap'; import styled from 'styled-components'; +import { StyledIconWrapper } from '@/components/common/styles'; + export interface IAssociationHeaderProps { icon: React.ReactNode; title: string; @@ -30,20 +32,3 @@ export default AssociationHeader; const StyledAssociationHeaderWrapper = styled.div` color: ${props => props.theme.bcTokens.iconsColorSecondary}; `; - -const StyledIconWrapper = styled.div` - color: white; - background-color: ${props => props.theme.css.activeActionColor}; - - font-size: 1.5rem; - - border-radius: 50%; - - width: 2.5rem; - height: 2.5rem; - padding: 1rem; - - display: flex; - justify-content: center; - align-items: center; -`; diff --git a/source/frontend/src/features/mapSideBar/property/tabs/propertyAssociations/__snapshots__/AssociationHeader.test.tsx.snap b/source/frontend/src/features/mapSideBar/property/tabs/propertyAssociations/__snapshots__/AssociationHeader.test.tsx.snap index 1965c8d91c..eda25988c4 100644 --- a/source/frontend/src/features/mapSideBar/property/tabs/propertyAssociations/__snapshots__/AssociationHeader.test.tsx.snap +++ b/source/frontend/src/features/mapSideBar/property/tabs/propertyAssociations/__snapshots__/AssociationHeader.test.tsx.snap @@ -6,17 +6,13 @@ exports[`AssociationHeader component > renders as expected when provided valid d class="Toastify" />
- .c0 { - color: #474543; -} - -.c1 { - color: white; - background-color: #428bca; + .c1 { + background-color: #fcba19; font-size: 1.5rem; border-radius: 50%; - width: 2.5rem; - height: 2.5rem; + opacity: 0.8; + width: 3.25rem; + height: 3.25rem; padding: 1rem; display: -webkit-box; display: -webkit-flex; @@ -30,6 +26,15 @@ exports[`AssociationHeader component > renders as expected when provided valid d -webkit-box-align: center; -ms-flex-align: center; align-items: center; + font-family: 'BCSans-Bold'; +} + +.c1.selected { + background-color: #FCBA19; +} + +.c0 { + color: #474543; }
renders as expected when provide border-radius: 0.5rem; } -.c3 { - color: #474543; -} - .c4 { - color: white; - background-color: #428bca; + background-color: #fcba19; font-size: 1.5rem; border-radius: 50%; - width: 2.5rem; - height: 2.5rem; + opacity: 0.8; + width: 3.25rem; + height: 3.25rem; padding: 1rem; display: -webkit-box; display: -webkit-flex; @@ -56,6 +52,15 @@ exports[`PropertyAssociationTabView component > renders as expected when provide -webkit-box-align: center; -ms-flex-align: center; align-items: center; + font-family: 'BCSans-Bold'; +} + +.c4.selected { + background-color: #FCBA19; +} + +.c3 { + color: #474543; }
- + - + renders as expected 1`] = `
renders as expected 1`] = `
void, onDelete: (activityId: number) => void, ) { @@ -109,7 +110,7 @@ export function activityActionColumn( const { hasClaim } = useKeycloakWrapper(); const activityRow = cellProps.row.original; const renderDelete = () => { - if (hasClaim(Claims.MANAGEMENT_DELETE) && exists(onDelete)) { + if (hasClaim(Claims.MANAGEMENT_DELETE) && canEdit && exists(onDelete)) { if ( activityRow?.activityStatusType?.id === PropertyManagementActivityStatusTypes.NOTSTARTED ) { diff --git a/source/frontend/src/features/mapSideBar/property/tabs/propertyDetailsManagement/activity/list/ManagementActivitiesListContainer.tsx b/source/frontend/src/features/mapSideBar/property/tabs/propertyDetailsManagement/activity/list/ManagementActivitiesListContainer.tsx index 7390a00f4c..8cb0d17a75 100644 --- a/source/frontend/src/features/mapSideBar/property/tabs/propertyDetailsManagement/activity/list/ManagementActivitiesListContainer.tsx +++ b/source/frontend/src/features/mapSideBar/property/tabs/propertyDetailsManagement/activity/list/ManagementActivitiesListContainer.tsx @@ -3,6 +3,7 @@ import { useHistory } from 'react-router-dom'; import { TableSort } from '@/components/Table/TableSort'; import { SideBarContext } from '@/features/mapSideBar/context/sidebarContext'; +import ManagementStatusUpdateSolver from '@/features/mapSideBar/management/tabs/fileDetails/detail/ManagementStatusUpdateSolver'; import { usePropertyActivityRepository } from '@/hooks/repositories/usePropertyActivityRepository'; import { getDeleteModalProps, useModalContext } from '@/hooks/useModalContext'; import useIsMounted from '@/hooks/util/useIsMounted'; @@ -13,6 +14,7 @@ import { IManagementActivitiesListViewProps } from './ManagementActivitiesListVi import { PropertyActivityRow } from './models/PropertyActivityRow'; export interface IPropertyManagementActivitiesListContainerProps { + statusSolver?: ManagementStatusUpdateSolver; propertyId: number; isAdHoc?: boolean; View: React.FC; @@ -20,7 +22,7 @@ export interface IPropertyManagementActivitiesListContainerProps { const PropertyManagementActivitiesListContainer: React.FunctionComponent< IPropertyManagementActivitiesListContainerProps -> = ({ propertyId, isAdHoc, View }) => { +> = ({ statusSolver, propertyId, isAdHoc, View }) => { const history = useHistory(); const isMounted = useIsMounted(); const { setModalContent, setDisplayModal } = useModalContext(); @@ -64,6 +66,8 @@ const PropertyManagementActivitiesListContainer: React.FunctionComponent< history.push(`/mapview/sidebar/property/${propertyId}/management/activity/${activityId}`); }; + const canEditActivities = !statusSolver || statusSolver?.canEditActivities(); + return ( ); }; diff --git a/source/frontend/src/features/mapSideBar/property/tabs/propertyDetailsManagement/activity/list/ManagementActivitiesListView.test.tsx b/source/frontend/src/features/mapSideBar/property/tabs/propertyDetailsManagement/activity/list/ManagementActivitiesListView.test.tsx index e428488631..bdef27bdb1 100644 --- a/source/frontend/src/features/mapSideBar/property/tabs/propertyDetailsManagement/activity/list/ManagementActivitiesListView.test.tsx +++ b/source/frontend/src/features/mapSideBar/property/tabs/propertyDetailsManagement/activity/list/ManagementActivitiesListView.test.tsx @@ -40,6 +40,7 @@ describe('Activities list view', () => { onView={onView} setSort={setSort} sort={undefined} + canEditActivities={renderOptions?.canEditActivities ?? true} />, { ...renderOptions, diff --git a/source/frontend/src/features/mapSideBar/property/tabs/propertyDetailsManagement/activity/list/ManagementActivitiesListView.tsx b/source/frontend/src/features/mapSideBar/property/tabs/propertyDetailsManagement/activity/list/ManagementActivitiesListView.tsx index 2c360c09b7..990d02a952 100644 --- a/source/frontend/src/features/mapSideBar/property/tabs/propertyDetailsManagement/activity/list/ManagementActivitiesListView.tsx +++ b/source/frontend/src/features/mapSideBar/property/tabs/propertyDetailsManagement/activity/list/ManagementActivitiesListView.tsx @@ -17,6 +17,7 @@ export interface IManagementActivitiesListViewProps { isLoading: boolean; propertyActivities: PropertyActivityRow[]; sort: TableSort; + canEditActivities: boolean; getNavigationUrl?: (row: PropertyActivityRow) => { title: string; url: string }; setSort: React.Dispatch>>; onCreate?: () => void; @@ -28,6 +29,7 @@ const ManagementActivitiesListView: React.FunctionComponent} - onAdd={onCreate} - /> + canEditActivities ? ( + } + onAdd={onCreate} + isAddEnabled={canEditActivities} + /> + ) : ( + 'Activities List' + ) } >
); diff --git a/source/frontend/src/features/mapSideBar/property/tabs/propertyDetailsManagement/detail/PropertyContactListView.tsx b/source/frontend/src/features/mapSideBar/property/tabs/propertyDetailsManagement/detail/PropertyContactListView.tsx index d08767c371..76731dbbd8 100644 --- a/source/frontend/src/features/mapSideBar/property/tabs/propertyDetailsManagement/detail/PropertyContactListView.tsx +++ b/source/frontend/src/features/mapSideBar/property/tabs/propertyDetailsManagement/detail/PropertyContactListView.tsx @@ -6,6 +6,7 @@ import { Section } from '@/components/common/Section/Section'; import { SectionListHeader } from '@/components/common/SectionListHeader'; import Claims from '@/constants/claims'; import { ApiGen_Concepts_PropertyContact } from '@/models/api/generated/ApiGen_Concepts_PropertyContact'; +import { exists } from '@/utils'; import { InventoryTabNames } from '../../../InventoryTabs'; import { PropertyEditForms } from '../../../PropertyRouter'; @@ -23,7 +24,8 @@ export const PropertyContactListView: React.FunctionComponent { const history = useHistory(); - const match = useRouteMatch<{ propertyId: string }>(); + const matchProperty = useRouteMatch<{ propertyId: string }>(); + const matchPropertyFile = useRouteMatch<{ id: string; menuIndex: string }>(); return (
} onAdd={() => { - const path = generatePath(match.path, { - propertyId: match.params.propertyId, - tab: InventoryTabNames.management, - }); - history.push(`${path}/${PropertyEditForms.UpdateContactContainer}?edit=true`); + if (exists(matchProperty.params.propertyId)) { + const path = generatePath(matchProperty.path, { + propertyId: matchProperty.params.propertyId, + tab: InventoryTabNames.management, + }); + history.push(`${path}/${PropertyEditForms.UpdateContactContainer}?edit=true`); + } else { + const path = generatePath(matchPropertyFile.path, { + id: matchPropertyFile.params.id, + menuIndex: matchPropertyFile.params.menuIndex, + tab: InventoryTabNames.management, + }); + history.push(`${path}/${PropertyEditForms.UpdateContactContainer}?edit=true`); + } }} /> } @@ -48,13 +59,24 @@ export const PropertyContactListView: React.FunctionComponent { - const path = generatePath(match.path, { - propertyId: match.params.propertyId, - tab: InventoryTabNames.management, - }); - history.push( - `${path}/${PropertyEditForms.UpdateContactContainer}/${contactId}?edit=true`, - ); + if (exists(matchProperty.params.propertyId)) { + const path = generatePath(matchProperty.path, { + propertyId: matchProperty.params.propertyId, + tab: InventoryTabNames.management, + }); + history.push( + `${path}/${PropertyEditForms.UpdateContactContainer}/${contactId}?edit=true`, + ); + } else { + const path = generatePath(matchPropertyFile.path, { + id: matchPropertyFile.params.id, + menuIndex: matchPropertyFile.params.menuIndex, + tab: InventoryTabNames.management, + }); + history.push( + `${path}/${PropertyEditForms.UpdateContactContainer}/${contactId}?edit=true`, + ); + } }} handleDelete={onDelete} /> diff --git a/source/frontend/src/features/mapSideBar/property/tabs/propertyResearch/detail/PropertyResearchTabView.test.tsx b/source/frontend/src/features/mapSideBar/property/tabs/propertyResearch/detail/PropertyResearchTabView.test.tsx index 424fc82bb0..6cf02638e1 100644 --- a/source/frontend/src/features/mapSideBar/property/tabs/propertyResearch/detail/PropertyResearchTabView.test.tsx +++ b/source/frontend/src/features/mapSideBar/property/tabs/propertyResearch/detail/PropertyResearchTabView.test.tsx @@ -59,6 +59,7 @@ describe('PropertyResearchTabView component', () => { const fakePropertyResearch: ApiGen_Concepts_ResearchFileProperty = { id: 0, + isActive: null, propertyId: 1, property: getEmptyProperty(), file: getEmptyResearchFile(), diff --git a/source/frontend/src/features/mapSideBar/property/tabs/propertyResearch/update/models.ts b/source/frontend/src/features/mapSideBar/property/tabs/propertyResearch/update/models.ts index b70b7d3472..0cf35a2690 100644 --- a/source/frontend/src/features/mapSideBar/property/tabs/propertyResearch/update/models.ts +++ b/source/frontend/src/features/mapSideBar/property/tabs/propertyResearch/update/models.ts @@ -113,6 +113,7 @@ export class UpdatePropertyFormModel { researchSummary: this.researchSummary ?? null, property: null, location: null, + isActive: null, fileId: this.researchFileId ?? 0, file: { ...getEmptyResearchFile(), rowVersion: this.researchFileRowVersion }, propertyResearchPurposeTypes: this.propertyResearchPurposeTypes?.map(x => x.toApi()) ?? null, diff --git a/source/frontend/src/features/mapSideBar/research/ResearchContainer.tsx b/source/frontend/src/features/mapSideBar/research/ResearchContainer.tsx index 46d010f072..154c71ed11 100644 --- a/source/frontend/src/features/mapSideBar/research/ResearchContainer.tsx +++ b/source/frontend/src/features/mapSideBar/research/ResearchContainer.tsx @@ -169,22 +169,16 @@ export const ResearchContainer: React.FunctionComponent return; } - const fileProperties = researchFile.fileProperties ?? []; - const menuIndex = fileProperties.findIndex(fp => fp.id === filePropertyId); - if (menuIndex < 0) { - return; - } - if (isEditing) { if (formikRef?.current?.dirty) { handleCancelClick(() => - pathGenerator.showFilePropertyIndex('research', researchFile.id, menuIndex + 1), + pathGenerator.showFilePropertyId('research', researchFile.id, filePropertyId), ); return; } } // The index needs to be offset to match the menu index - pathGenerator.showFilePropertyIndex('research', researchFile.id, menuIndex + 1); + pathGenerator.showFilePropertyId('research', researchFile.id, filePropertyId); }; const onEditProperties = () => { diff --git a/source/frontend/src/features/mapSideBar/research/ResearchView.tsx b/source/frontend/src/features/mapSideBar/research/ResearchView.tsx index 762bf20d25..5a0ebe8416 100644 --- a/source/frontend/src/features/mapSideBar/research/ResearchView.tsx +++ b/source/frontend/src/features/mapSideBar/research/ResearchView.tsx @@ -21,7 +21,7 @@ import { PropertyForm } from '../shared/models'; import SidebarFooter from '../shared/SidebarFooter'; import { StyledFormWrapper } from '../shared/styles'; import UpdateProperties from '../shared/update/properties/UpdateProperties'; -import { usePropertyIndexFromUrl } from '../shared/usePropertyIndexFromUrl'; +import { useFilePropertyIdFromUrl } from '../shared/usePropertyIndexFromUrl'; import ResearchHeader from './common/ResearchHeader'; import ResearchRouter from './ResearchRouter'; import ResearchStatusUpdateSolver from './tabs/fileDetails/ResearchStatusUpdateSolver'; @@ -76,7 +76,7 @@ const ResearchView: React.FunctionComponent = ({ // Extract the zero-based property index from the current URL path. // It will be null if route is not matched - const currentPropertyIndex: number | null = usePropertyIndexFromUrl(); + const currentFilePropertyId: number | null = useFilePropertyIdFromUrl(); const statusSolver = new ResearchStatusUpdateSolver(researchFile); return ( @@ -122,7 +122,7 @@ const ResearchView: React.FunctionComponent = ({ leftComponent={ = ({ researchFile={researchFile} /> ( diff --git a/source/frontend/src/features/mapSideBar/research/__snapshots__/ResearchContainer.test.tsx.snap b/source/frontend/src/features/mapSideBar/research/__snapshots__/ResearchContainer.test.tsx.snap index a8d2b089e5..d43fc2e87d 100644 --- a/source/frontend/src/features/mapSideBar/research/__snapshots__/ResearchContainer.test.tsx.snap +++ b/source/frontend/src/features/mapSideBar/research/__snapshots__/ResearchContainer.test.tsx.snap @@ -950,11 +950,51 @@ exports[`ResearchContainer component > renders as expected 1`] = `
- - Properties - +
+ + Properties + +
+
+
+ +
+
renders as expected 1`] = `
+ Active
renders as expected 1`] = ` >
+
+ +
diff --git a/source/frontend/src/features/mapSideBar/research/add/AddResearchContainer.test.tsx b/source/frontend/src/features/mapSideBar/research/add/AddResearchContainer.test.tsx index 3778dc800a..e2a07646bb 100644 --- a/source/frontend/src/features/mapSideBar/research/add/AddResearchContainer.test.tsx +++ b/source/frontend/src/features/mapSideBar/research/add/AddResearchContainer.test.tsx @@ -5,10 +5,7 @@ import noop from 'lodash/noop'; import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; -import { - IMapStateMachineContext, - useMapStateMachine, -} from '@/components/common/mapFSM/MapStateMachineContext'; +import { IMapStateMachineContext } from '@/components/common/mapFSM/MapStateMachineContext'; import { mapMachineBaseMock } from '@/mocks/mapFSM.mock'; import { PMBC_FullyAttributed_Feature_Properties } from '@/models/layers/parcelMapBC'; import { RenderOptions, act, renderAsync, userEvent, waitFor } from '@/utils/test-utils'; diff --git a/source/frontend/src/features/mapSideBar/research/add/__snapshots__/ResearchProperties.test.tsx.snap b/source/frontend/src/features/mapSideBar/research/add/__snapshots__/ResearchProperties.test.tsx.snap index 937c60053c..1694398a7b 100644 --- a/source/frontend/src/features/mapSideBar/research/add/__snapshots__/ResearchProperties.test.tsx.snap +++ b/source/frontend/src/features/mapSideBar/research/add/__snapshots__/ResearchProperties.test.tsx.snap @@ -214,54 +214,54 @@ exports[`ResearchProperties component > renders as expected when provided no pro margin-right: 0; } -.c21.c21.btn { +.c22.c22.btn { background-color: unset; border: none; } -.c21.c21.btn:hover, -.c21.c21.btn:focus, -.c21.c21.btn:active { +.c22.c22.btn:hover, +.c22.c22.btn:focus, +.c22.c22.btn:active { background-color: unset; outline: none; box-shadow: none; } -.c21.c21.btn svg { +.c22.c22.btn svg { -webkit-transition: all 0.3s ease-out; transition: all 0.3s ease-out; } -.c21.c21.btn svg:hover { +.c22.c22.btn svg:hover { -webkit-transition: all 0.3s ease-in; transition: all 0.3s ease-in; } -.c21.c21.btn.btn-primary svg { +.c22.c22.btn.btn-primary svg { color: #013366; } -.c21.c21.btn.btn-primary svg:hover { +.c22.c22.btn.btn-primary svg:hover { color: #013366; } -.c21.c21.btn.btn-light svg { +.c22.c22.btn.btn-light svg { color: var(--surface-color-primary-button-default); } -.c21.c21.btn.btn-light svg:hover { +.c22.c22.btn.btn-light svg:hover { color: #CE3E39; } -.c21.c21.btn.btn-info svg { +.c22.c22.btn.btn-info svg { color: var(--surface-color-primary-button-default); } -.c21.c21.btn.btn-info svg:hover { +.c22.c22.btn.btn-info svg:hover { color: var(--surface-color-primary-button-hover); } -.c22.c22.btn { +.c23.c23.btn { font-size: 1.4rem; color: #aaaaaa; -webkit-text-decoration: none; @@ -269,13 +269,13 @@ exports[`ResearchProperties component > renders as expected when provided no pro line-height: unset; } -.c22.c22.btn .text { +.c23.c23.btn .text { display: none; } -.c22.c22.btn:hover, -.c22.c22.btn:active, -.c22.c22.btn:focus { +.c23.c23.btn:hover, +.c23.c23.btn:active, +.c23.c23.btn:focus { color: #d8292f; -webkit-text-decoration: none; text-decoration: none; @@ -289,9 +289,9 @@ exports[`ResearchProperties component > renders as expected when provided no pro flex-direction: row; } -.c22.c22.btn:hover .text, -.c22.c22.btn:active .text, -.c22.c22.btn:focus .text { +.c23.c23.btn:hover .text, +.c23.c23.btn:active .text, +.c23.c23.btn:focus .text { display: inline; line-height: 2rem; } @@ -315,7 +315,7 @@ exports[`ResearchProperties component > renders as expected when provided no pro border-bottom-left-radius: 0; } -.c20 { +.c21 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -327,7 +327,7 @@ exports[`ResearchProperties component > renders as expected when provided no pro gap: 0.8rem; } -.c20 .form-label { +.c21 .form-label { -webkit-flex-shrink: 0; -ms-flex-negative: 0; flex-shrink: 0; @@ -474,21 +474,25 @@ exports[`ResearchProperties component > renders as expected when provided no pro font-family: 'BcSans-Bold'; } -.c19 { +.c20 { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.c17 { +.c18 { min-width: 3rem; min-height: 4.5rem; } -.c18 { +.c19 { font-size: 1.2rem; } +.c17 { + min-height: 4.5rem; +} +
@@ -1091,7 +1095,7 @@ exports[`ResearchProperties component > renders as expected when provided no pro
renders as expected when provided no pro class="mb-0 d-flex align-items-center" > renders as expected when provided no pro renders as expected when provided no pro
PID: 123-456-789
@@ -1180,7 +1184,7 @@ exports[`ResearchProperties component > renders as expected when provided no pro class="col-md-5" >
renders as expected when provided no pro />
+
+ +
renders as expected when provided no pro class="mb-0 d-flex align-items-center" > renders as expected when provided no pro renders as expected when provided no pro
PIN: 1111222
@@ -1349,7 +1380,7 @@ exports[`ResearchProperties component > renders as expected when provided no pro class="col-md-5" >
renders as expected when provided no pro />
+
+ +
+
+
matches snapshot 1`] = `
+ Active
- - - -
+ />
1
@@ -377,16 +404,52 @@ exports[`FileMenuView component > matches snapshot 1`] = `
- - 023-214-937 - +
+ 023-214-937 +
+ +
+
+
matches snapshot 1`] = ` >
+
+ +
diff --git a/source/frontend/src/features/mapSideBar/shared/detail/PropertyFileContainer.tsx b/source/frontend/src/features/mapSideBar/shared/detail/PropertyFileContainer.tsx index 956c0e5ab2..47d4a87bfb 100644 --- a/source/frontend/src/features/mapSideBar/shared/detail/PropertyFileContainer.tsx +++ b/source/frontend/src/features/mapSideBar/shared/detail/PropertyFileContainer.tsx @@ -26,10 +26,11 @@ import useKeycloakWrapper from '@/hooks/useKeycloakWrapper'; import { ApiGen_CodeTypes_FileTypes } from '@/models/api/generated/ApiGen_CodeTypes_FileTypes'; import { ApiGen_Concepts_FileProperty } from '@/models/api/generated/ApiGen_Concepts_FileProperty'; import { ApiGen_Concepts_ResearchFileProperty } from '@/models/api/generated/ApiGen_Concepts_ResearchFileProperty'; -import { exists, getLatLng, isValidId } from '@/utils'; +import { exists, getLatLng, isPlanNumberSPCP, isValidId } from '@/utils'; import { getLeaseInfo, LeaseAssociationInfo } from '../../property/PropertyContainer'; import CrownDetailsTabView from '../../property/tabs/crown/CrownDetailsTabView'; +import LtsaPlanTabView from '../../property/tabs/ltsa/LtsaPlanTabView'; import { PropertyManagementTabView } from '../../property/tabs/propertyDetailsManagement/detail/PropertyManagementTabView'; import PropertyResearchTabView from '../../property/tabs/propertyResearch/detail/PropertyResearchTabView'; import ResearchStatusUpdateSolver from '../../research/tabs/fileDetails/ResearchStatusUpdateSolver'; @@ -51,6 +52,7 @@ export const PropertyFileContainer: React.FunctionComponent< > = props => { const pid = props.fileProperty?.property?.pid ?? undefined; const id = props.fileProperty?.property?.id ?? undefined; + const planNumber = props.fileProperty?.property?.planNumber ?? undefined; const location = props.fileProperty?.property?.location ?? undefined; const latLng = useMemo(() => getLatLng(location) ?? undefined, [location]); const { hasClaim } = useKeycloakWrapper(); @@ -82,6 +84,7 @@ export const PropertyFileContainer: React.FunctionComponent< const leaseAssociations = composedProperties?.propertyAssociationWrapper?.response?.leaseAssociations; + useMemo( () => hasClaim(Claims.LEASE_VIEW) @@ -122,6 +125,21 @@ export const PropertyFileContainer: React.FunctionComponent< name: 'Title', }); + if (exists(planNumber) && isPlanNumberSPCP(planNumber)) { + tabViews.push({ + content: ( + + ), + key: InventoryTabNames.plan, + name: 'Plan', + }); + } + if (exists(composedProperties.composedProperty?.crownTenureFeatures)) { tabViews.push({ content: ( @@ -188,6 +206,7 @@ export const PropertyFileContainer: React.FunctionComponent< name: 'Property Details', }); } + if (isValidId(id)) { tabViews.push({ content: ( @@ -242,7 +261,7 @@ export const PropertyFileContainer: React.FunctionComponent< type={NoteTypes.Property} entityId={composedProperties.apiWrapper.response.id} onSuccess={props.onChildSuccess} - NoteListView={NoteListView} + View={NoteListView} /> ) { Object.assign(this, baseModel); @@ -116,6 +117,7 @@ export class PropertyForm { formattedAddress: model.address, landArea: model.landArea, areaUnit: model.areaUnit, + isActive: model.isActive !== false ? 'true' : 'false', }); } @@ -160,6 +162,8 @@ export class PropertyForm { pimsFeature?.properties?.LAND_LEGAL_DESCRIPTION ?? parcelFeature?.properties?.LEGAL_DESCRIPTION ?? '', + isActive: model.isActive !== false ? 'true' : 'false', + displayOrder: model.displayOrder, }); } @@ -178,6 +182,7 @@ export class PropertyForm { districtName: this.districtName, legalDescription: this.legalDescription, address: this.address ? formatApiAddress(this.address.toApi()) : this.formattedAddress, + isActive: this.isActive !== 'false', }; } @@ -237,6 +242,7 @@ export class PropertyForm { geometry: null, }, municipalityFeature: null, + isActive: this.isActive !== 'false', }; } @@ -269,6 +275,7 @@ export class PropertyForm { : undefined; newForm.legalDescription = model.property?.landLegalDescription ?? undefined; newForm.isRetired = model.property?.isRetired ?? undefined; + newForm.isActive = model.isActive !== false ? 'true' : 'false'; return newForm; } @@ -308,6 +315,7 @@ export class PropertyForm { propertyName: this.name ?? null, location: latLngToApiLocation(this.fileLocation?.lat, this.fileLocation?.lng), displayOrder: this.displayOrder ?? null, + isActive: this.isActive !== 'false', rowVersion: this.rowVersion ?? null, }; } diff --git a/source/frontend/src/features/mapSideBar/shared/operations/SelectedOperationProperty.tsx b/source/frontend/src/features/mapSideBar/shared/operations/SelectedOperationProperty.tsx index 645738bdd6..c14ad702f6 100644 --- a/source/frontend/src/features/mapSideBar/shared/operations/SelectedOperationProperty.tsx +++ b/source/frontend/src/features/mapSideBar/shared/operations/SelectedOperationProperty.tsx @@ -1,12 +1,23 @@ +import { geoJSON } from 'leaflet'; +import { useCallback } from 'react'; import { Col, Row } from 'react-bootstrap'; +import { FaSearchPlus } from 'react-icons/fa'; -import { RemoveButton } from '@/components/common/buttons'; +import { LinkButton, RemoveButton } from '@/components/common/buttons'; import { InlineInput } from '@/components/common/form/styles'; +import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; import OverflowTip from '@/components/common/OverflowTip'; import DraftCircleNumber from '@/components/propertySelector/selectedPropertyList/DraftCircleNumber'; import { AreaUnitTypes } from '@/constants'; import { ApiGen_Concepts_Property } from '@/models/api/generated/ApiGen_Concepts_Property'; -import { convertArea, formatApiAddress, formatNumber, pidFormatter } from '@/utils'; +import { + convertArea, + exists, + formatApiAddress, + formatNumber, + pidFormatter, + pimsGeomeryToGeometry, +} from '@/utils'; interface ISelectedOperationPropertyProps { property: ApiGen_Concepts_Property; @@ -24,6 +35,20 @@ export const SelectedOperationProperty: React.FunctionComponent< return formatNumber(sqm, 0, 4); }; + const mapMachine = useMapStateMachine(); + + const onZoomToProperty = useCallback( + (property: ApiGen_Concepts_Property) => { + const geom = property?.boundary ?? pimsGeomeryToGeometry(property?.location); + const bounds = geoJSON(geom).getBounds(); + + if (exists(bounds)) { + mapMachine.requestFlyToBounds(bounds); + } + }, + [mapMachine], + ); + return ( @@ -44,7 +69,12 @@ export const SelectedOperationProperty: React.FunctionComponent< )} {formatApiAddress(property?.address) ?? ''} - + + onZoomToProperty(property)}> + + + + diff --git a/source/frontend/src/features/mapSideBar/shared/sidebarPathGenerator.ts b/source/frontend/src/features/mapSideBar/shared/sidebarPathGenerator.ts index 3f2c9cae36..501124783d 100644 --- a/source/frontend/src/features/mapSideBar/shared/sidebarPathGenerator.ts +++ b/source/frontend/src/features/mapSideBar/shared/sidebarPathGenerator.ts @@ -17,7 +17,7 @@ export interface IPathGeneratorMethods { editDetail: (fileType: string, fileId: number, detailType: string, detailId: number) => void; addDetail: (fileType: string, fileId: number, detailType: string) => void; editProperties: (fileType: string, fileId: number) => void; - showFilePropertyIndex: (fileType: string, fileId: number, propertyIndex: number) => void; + showFilePropertyId: (fileType: string, fileId: number, filePropertyId: number) => void; showFilePropertyDetail: ( fileType: string, fileId: number, @@ -132,7 +132,7 @@ const usePathGenerator: IPathGenerator = () => { history.push(path); }; - const showFilePropertyIndex = (fileType: string, fileId: number, menuIndex: number) => { + const showFilePropertyId = (fileType: string, fileId: number, menuIndex: number) => { const a = `${sidebarBasePath}/:fileType/:fileId/property/:menuIndex`; const path = generatePath(a, { fileType, @@ -174,7 +174,7 @@ const usePathGenerator: IPathGenerator = () => { editDetails, addDetail, editProperties, - showFilePropertyIndex, + showFilePropertyId, showFilePropertyDetail, }; }; diff --git a/source/frontend/src/features/mapSideBar/shared/tabs/DocumentsTab.tsx b/source/frontend/src/features/mapSideBar/shared/tabs/DocumentsTab.tsx index 8c72b42f31..f469c7ab60 100644 --- a/source/frontend/src/features/mapSideBar/shared/tabs/DocumentsTab.tsx +++ b/source/frontend/src/features/mapSideBar/shared/tabs/DocumentsTab.tsx @@ -1,16 +1,19 @@ import DocumentListContainer from '@/features/documents/list/DocumentListContainer'; +import { IUpdateDocumentsStrategy } from '@/features/documents/models/IUpdateDocumentsStrategy'; import { ApiGen_CodeTypes_DocumentRelationType } from '@/models/api/generated/ApiGen_CodeTypes_DocumentRelationType'; interface IDocumentsTabProps { fileId: number; relationshipType: ApiGen_CodeTypes_DocumentRelationType; title?: string; + statusSolver?: IUpdateDocumentsStrategy; onSuccess?: () => void; } const DocumentsTab: React.FunctionComponent = ({ fileId, relationshipType, + statusSolver, title = 'File Documents', onSuccess, }) => { @@ -19,6 +22,7 @@ const DocumentsTab: React.FunctionComponent = ({ title={title} parentId={fileId.toString()} relationshipType={relationshipType} + statusSolver={statusSolver} onSuccess={onSuccess} /> ); diff --git a/source/frontend/src/features/mapSideBar/shared/tabs/ManagementDocumentsTab.tsx b/source/frontend/src/features/mapSideBar/shared/tabs/ManagementDocumentsTab.tsx index 347b855222..f6b6efd560 100644 --- a/source/frontend/src/features/mapSideBar/shared/tabs/ManagementDocumentsTab.tsx +++ b/source/frontend/src/features/mapSideBar/shared/tabs/ManagementDocumentsTab.tsx @@ -1,26 +1,33 @@ import DocumentListContainer from '@/features/documents/list/DocumentListContainer'; import DocumentManagementListContainer from '@/features/documents/list/DocumentManagementListContainer'; import { ApiGen_CodeTypes_DocumentRelationType } from '@/models/api/generated/ApiGen_CodeTypes_DocumentRelationType'; +import { ApiGen_Concepts_ManagementFile } from '@/models/api/generated/ApiGen_Concepts_ManagementFile'; + +import ManagementStatusUpdateSolver from '../../management/tabs/fileDetails/detail/ManagementStatusUpdateSolver'; interface IManagementDocumentsTabProps { - fileId: number; + managementFile: ApiGen_Concepts_ManagementFile; onSuccess?: () => void; } const ManagementDocumentsTab: React.FunctionComponent = ({ - fileId, + managementFile, onSuccess, }) => { + const statusSolver = new ManagementStatusUpdateSolver(managementFile); + return ( <> { fileProperties: [ { id: 3, + isActive: null, propertyId: 443, property: { ...getMockApiProperty(), diff --git a/source/frontend/src/features/mapSideBar/shared/update/properties/UpdateProperties.tsx b/source/frontend/src/features/mapSideBar/shared/update/properties/UpdateProperties.tsx index 045cc282a4..c0c5f45017 100644 --- a/source/frontend/src/features/mapSideBar/shared/update/properties/UpdateProperties.tsx +++ b/source/frontend/src/features/mapSideBar/shared/update/properties/UpdateProperties.tsx @@ -1,17 +1,20 @@ import axios, { AxiosError } from 'axios'; import { FieldArray, Formik, FormikProps } from 'formik'; -import { LatLngLiteral } from 'leaflet'; +import { geoJSON, LatLngLiteral } from 'leaflet'; import isNumber from 'lodash/isNumber'; import { useContext, useRef, useState } from 'react'; import { Col, Row } from 'react-bootstrap'; +import { PiCornersOut } from 'react-icons/pi'; import { toast } from 'react-toastify'; +import { LinkButton } from '@/components/common/buttons/LinkButton'; import GenericModal from '@/components/common/GenericModal'; import LoadingBackdrop from '@/components/common/LoadingBackdrop'; +import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; import { SelectedFeatureDataset } from '@/components/common/mapFSM/useLocationFeatureLoader'; import { Section } from '@/components/common/Section/Section'; +import TooltipWrapper from '@/components/common/TooltipWrapper'; import MapSelectorContainer from '@/components/propertySelector/MapSelectorContainer'; -import SelectedPropertyHeaderRow from '@/components/propertySelector/selectedPropertyList/SelectedPropertyHeaderRow'; import SelectedPropertyRow from '@/components/propertySelector/selectedPropertyList/SelectedPropertyRow'; import { SideBarContext } from '@/features/mapSideBar/context/sidebarContext'; import MapSideBarLayout from '@/features/mapSideBar/layout/MapSideBarLayout'; @@ -19,7 +22,7 @@ import { useBcaAddress } from '@/features/properties/map/hooks/useBcaAddress'; import { getCancelModalProps, useModalContext } from '@/hooks/useModalContext'; import { ApiGen_Concepts_File } from '@/models/api/generated/ApiGen_Concepts_File'; import { UserOverrideCode } from '@/models/api/UserOverrideCode'; -import { isLatLngInFeatureSetBoundary, isValidId } from '@/utils'; +import { exists, isLatLngInFeatureSetBoundary, isValidId, latLngLiteralToGeometry } from '@/utils'; import { AddressForm, FileForm, PropertyForm } from '../../models'; import SidebarFooter from '../../SidebarFooter'; @@ -37,6 +40,7 @@ export interface IUpdatePropertiesProps { confirmBeforeAdd: (propertyForm: PropertyForm) => Promise; confirmBeforeAddMessage?: React.ReactNode; formikRef?: React.RefObject>; + disableProperties?: boolean; } export const UpdateProperties: React.FunctionComponent = props => { @@ -50,6 +54,22 @@ export const UpdateProperties: React.FunctionComponent = const { setModalContent, setDisplayModal } = useModalContext(); const { resetFilePropertyLocations } = useContext(SideBarContext); const { getPrimaryAddressByPid, bcaLoading } = useBcaAddress(); + const mapMachine = useMapStateMachine(); + + const fitBoundaries = () => { + const fileProperties = formikRef?.current?.values?.properties; + + if (exists(fileProperties)) { + const locations = fileProperties.map( + p => p?.polygon ?? latLngLiteralToGeometry(p?.fileLocation), + ); + const bounds = geoJSON(locations).getBounds(); + + if (exists(bounds) && bounds.isValid()) { + mapMachine.requestFlyToBounds(bounds); + } + } + }; const handleSaveClick = async () => { await formikRef?.current?.validateForm(); @@ -219,8 +239,23 @@ export const UpdateProperties: React.FunctionComponent = /> -
- +
+ Selected Properties + + + + + + + + + } + > {formikProps.values.properties.map((property, index) => ( = nameSpace={`properties.${index}`} index={index} property={property.toFeatureDataset()} + showDisable={props.disableProperties} /> ))} {formikProps.values.properties.length === 0 && ( diff --git a/source/frontend/src/features/mapSideBar/shared/update/properties/__snapshots__/UpdateProperties.test.tsx.snap b/source/frontend/src/features/mapSideBar/shared/update/properties/__snapshots__/UpdateProperties.test.tsx.snap index c246d287fc..324213f04d 100644 --- a/source/frontend/src/features/mapSideBar/shared/update/properties/__snapshots__/UpdateProperties.test.tsx.snap +++ b/source/frontend/src/features/mapSideBar/shared/update/properties/__snapshots__/UpdateProperties.test.tsx.snap @@ -482,15 +482,6 @@ exports[`UpdateProperties component > renders as expected 1`] = ` border-radius: 0.5rem; } -.c25 { - font-size: 1.6rem; - color: #9F9D9C; - border-bottom: 0.2rem solid #606060; - margin-bottom: 0.9rem; - padding-bottom: 0.25rem; - font-family: 'BcSans-Bold'; -} - .c28 { white-space: nowrap; overflow: hidden; @@ -506,6 +497,10 @@ exports[`UpdateProperties component > renders as expected 1`] = ` font-size: 1.2rem; } +.c25 { + min-height: 4.5rem; +} + .c5.btn.btn-light.Button { padding: 0; border: 0.1rem solid #9F9D9C; @@ -1232,7 +1227,43 @@ exports[`UpdateProperties component > renders as expected 1`] = `
- Selected properties +
+
+ Selected Properties +
+
+ +
+
@@ -1240,41 +1271,7 @@ exports[`UpdateProperties component > renders as expected 1`] = ` class="collapse show" >
-
- Identifier -
-
- Provide a descriptive name for this land - - - - - -
-
-
renders as expected 1`] = ` />
+
+ +
@@ -1402,7 +1426,7 @@ exports[`UpdateProperties component > renders as expected 1`] = `
+
+ diff --git a/source/frontend/src/features/notes/list/ManagementNoteSummaryContainer.tsx b/source/frontend/src/features/notes/list/ManagementNoteSummaryContainer.tsx index 4c112622be..607ca08388 100644 --- a/source/frontend/src/features/notes/list/ManagementNoteSummaryContainer.tsx +++ b/source/frontend/src/features/notes/list/ManagementNoteSummaryContainer.tsx @@ -9,12 +9,14 @@ import { ApiGen_Concepts_Association } from '@/models/api/generated/ApiGen_Conce import { ApiGen_Concepts_Note } from '@/models/api/generated/ApiGen_Concepts_Note'; import { isValidId } from '@/utils'; +import { IUpdateNotesStrategy } from '../models/IUpdateNotesStrategy'; import { sortNotes } from './NoteListContainer'; import { INoteListViewProps } from './NoteListView'; export interface INoteSummaryContainerProps { associationType: NoteTypes; entityId: number; + statusSolver?: IUpdateNotesStrategy; onSuccess?: () => void; NoteListView: React.FunctionComponent>; } @@ -23,13 +25,20 @@ export interface INoteSummaryContainerProps { * Container that retrieved a summary of notes from a management file, related to a property. * It retrieves the notes from the management file and displays them in a list. * @param entityId The ID of the entity to retrieve notes for. + * @param statusSolver The Status solver to determine if notes are editable. * @param onSuccess Callback function to be called when the notes are successfully retrieved. * @param NoteListView The component to display the list of notes. * @returns A React component that displays a summary of notes from a management file. */ export const NoteSummaryContainer: React.FunctionComponent< React.PropsWithChildren -> = ({ associationType, entityId, onSuccess, NoteListView }: INoteSummaryContainerProps) => { +> = ({ + associationType, + entityId, + statusSolver, + onSuccess, + NoteListView, +}: INoteSummaryContainerProps) => { const { execute: getAllPropertyAssociations, loading: loadingAssociations } = usePropertyAssociations(); const { @@ -84,6 +93,8 @@ export const NoteSummaryContainer: React.FunctionComponent< const loading = loadingNotes || loadingAssociations; + const editNotesEnabled = !statusSolver || statusSolver?.canEditNotes(); + return ( ); }; diff --git a/source/frontend/src/features/notes/list/ManagementNoteSummaryView.tsx b/source/frontend/src/features/notes/list/ManagementNoteSummaryView.tsx index 7756fd028f..30bf653eab 100644 --- a/source/frontend/src/features/notes/list/ManagementNoteSummaryView.tsx +++ b/source/frontend/src/features/notes/list/ManagementNoteSummaryView.tsx @@ -35,10 +35,14 @@ export const NoteListView: React.FunctionComponent { - setCurrentNote(note); - openViewNotes(); - }, null), + createNoteActionsColumn( + true, + (note: ApiGen_Concepts_Note) => { + setCurrentNote(note); + openViewNotes(); + }, + null, + ), ]; return ( diff --git a/source/frontend/src/features/notes/list/NoteListContainer.test.tsx b/source/frontend/src/features/notes/list/NoteListContainer.test.tsx index 0be22c6b40..dae73b2c9e 100644 --- a/source/frontend/src/features/notes/list/NoteListContainer.test.tsx +++ b/source/frontend/src/features/notes/list/NoteListContainer.test.tsx @@ -15,6 +15,8 @@ import { import NoteListContainer, { INoteListContainerProps } from './NoteListContainer'; import { NoteListView } from './NoteListView'; +import ManagementStatusUpdateSolver from '@/features/mapSideBar/management/tabs/fileDetails/detail/ManagementStatusUpdateSolver'; +import { IUpdateNotesStrategy } from '../models/IUpdateNotesStrategy'; vi.mock('@/hooks/repositories/useNoteRepository'); const mockGetAllNotesApi = getMockRepositoryObj([]); @@ -35,7 +37,7 @@ describe('Note List Container', () => { type={renderOptions?.type ?? NoteTypes.Acquisition_File} entityId={renderOptions?.entityId ?? 1} onSuccess={renderOptions?.onSuccess ?? onSuccess} - NoteListView={NoteListView} + View={NoteListView} />, { ...renderOptions, diff --git a/source/frontend/src/features/notes/list/NoteListContainer.tsx b/source/frontend/src/features/notes/list/NoteListContainer.tsx index d03313675b..f211b67ab2 100644 --- a/source/frontend/src/features/notes/list/NoteListContainer.tsx +++ b/source/frontend/src/features/notes/list/NoteListContainer.tsx @@ -8,13 +8,15 @@ import { useModalManagement } from '@/hooks/useModalManagement'; import { ApiGen_Concepts_Note } from '@/models/api/generated/ApiGen_Concepts_Note'; import { exists, isValidId } from '@/utils'; +import { IUpdateNotesStrategy } from '../models/IUpdateNotesStrategy'; import { INoteListViewProps } from './NoteListView'; export interface INoteListContainerProps { type: NoteTypes; entityId: number; + View: React.FunctionComponent>; + statusSolver?: IUpdateNotesStrategy | null; onSuccess?: () => void; - NoteListView: React.FunctionComponent>; } /** @@ -22,7 +24,7 @@ export interface INoteListContainerProps { */ export const NoteListContainer: React.FunctionComponent< React.PropsWithChildren -> = ({ type, entityId, onSuccess, NoteListView }: INoteListContainerProps) => { +> = ({ type, entityId, onSuccess, View, statusSolver }: INoteListContainerProps) => { const { getAllNotes: { execute: getAllNotes, loading: loadingNotes, response: notesResponse }, deleteNote: { execute: deleteNote, loading: loadingDeleteNote }, @@ -52,8 +54,10 @@ export const NoteListContainer: React.FunctionComponent< // UI components const loading = loadingNotes || loadingDeleteNote; + const editNotesEnabled = !statusSolver || statusSolver?.canEditNotes(); + return ( - ; + canEditNotes: boolean; openAddNotes?: () => void; closeAddNotes?: () => void; deleteNote?: (type: NoteTypes, noteId: number) => Promise; @@ -52,6 +54,7 @@ export const NoteListView: React.FunctionComponent { + const { hasClaim } = useKeycloakWrapper(); + const { setModalContent, setDisplayModal } = useModalContext(); const columns = [ ...createNoteTableColumns(), createNoteActionsColumn( + canEditNotes, (note: ApiGen_Concepts_Note) => { setCurrentNote(note); openViewNotes(); @@ -100,9 +106,9 @@ export const NoteListView: React.FunctionComponent { + if (hasClaim([Claims.NOTE_ADD]) && canEditNotes) { + return ( } onAdd={openAddNotes} /> - } - title="notes" - isCollapsable - initiallyExpanded - > + ); + } else { + return 'Notes'; + } + }; + + return ( +
= { resu sort={{}} results={results ?? []} setSort={setSort} - columns={[...createNoteTableColumns(), createNoteActionsColumn(onShowDetails, onDelete)]} + columns={[ + ...createNoteTableColumns(), + createNoteActionsColumn(true, onShowDetails, onDelete), + ]} />, { ...rest, diff --git a/source/frontend/src/features/notes/list/NoteResults/columns.tsx b/source/frontend/src/features/notes/list/NoteResults/columns.tsx index 2ecb512005..44e5bfe32e 100644 --- a/source/frontend/src/features/notes/list/NoteResults/columns.tsx +++ b/source/frontend/src/features/notes/list/NoteResults/columns.tsx @@ -44,6 +44,7 @@ export function createNoteTableColumns() { } export const createNoteActionsColumn = ( + canEditNotes: boolean, onShowDetails: (note: ApiGen_Concepts_Note) => void, onDelete: (note: ApiGen_Concepts_Note) => void, ) => ({ @@ -62,6 +63,7 @@ export const createNoteActionsColumn = ( onShowDetails(cellProps.row.original)} title="View Note" /> )} {hasClaim(Claims.NOTE_DELETE) && + canEditNotes && exists(onDelete) && !cellProps.row.original.isSystemGenerated && ( { - setCurrentNote(note); - openViewNotes(); - }, null), + createNoteActionsColumn( + false, + (note: ApiGen_Concepts_Note) => { + setCurrentNote(note); + openViewNotes(); + }, + null, + ), ]; return ( diff --git a/source/frontend/src/features/notes/models/IUpdateNotesStrategy.ts b/source/frontend/src/features/notes/models/IUpdateNotesStrategy.ts new file mode 100644 index 0000000000..7312cc41c1 --- /dev/null +++ b/source/frontend/src/features/notes/models/IUpdateNotesStrategy.ts @@ -0,0 +1,3 @@ +export interface IUpdateNotesStrategy { + canEditNotes(): boolean; +} diff --git a/source/frontend/src/features/properties/list/columns.tsx b/source/frontend/src/features/properties/list/columns.tsx index 162e49be01..233da2bccf 100644 --- a/source/frontend/src/features/properties/list/columns.tsx +++ b/source/frontend/src/features/properties/list/columns.tsx @@ -15,7 +15,13 @@ import { HistoricalNumberFieldView } from '@/features/mapSideBar/shared/header/H import useKeycloakWrapper from '@/hooks/useKeycloakWrapper'; import { ApiGen_Concepts_PropertyView } from '@/models/api/generated/ApiGen_Concepts_PropertyView'; import { ILookupCode } from '@/store/slices/lookupCodes'; -import { convertArea, formatNumber, formatSplitAddress, mapLookupCode } from '@/utils'; +import { + convertArea, + formatNumber, + formatSplitAddress, + mapLookupCode, + pidFormatter, +} from '@/utils'; export const ColumnDiv = styled.div` display: flex; @@ -38,7 +44,7 @@ export const columns = ({ Cell: (props: CellProps) => { return ( <> - {props.row.original.pid} + {pidFormatter(props?.row?.original?.pid?.toString())} {props.row.original.isRetired ? ( Renders the map 1`] = `
+
+
+ + +
+
diff --git a/source/frontend/src/features/research/list/ResearchListView.test.tsx b/source/frontend/src/features/research/list/ResearchListView.test.tsx index 73bbb77e1b..ea888c354a 100644 --- a/source/frontend/src/features/research/list/ResearchListView.test.tsx +++ b/source/frontend/src/features/research/list/ResearchListView.test.tsx @@ -643,6 +643,7 @@ const mockResearchListViewResponse: ApiGen_Concepts_ResearchFile[] = [ fileProperties: [ { id: 1, + isActive: null, propertyId: 1, property: { ...getMockApiProperty(), @@ -675,6 +676,7 @@ const mockResearchListViewResponse: ApiGen_Concepts_ResearchFile[] = [ }, { id: 2, + isActive: null, propertyId: 2, property: { ...getMockApiProperty(), diff --git a/source/frontend/src/hooks/pims-api/useApiLtsa.tsx b/source/frontend/src/hooks/pims-api/useApiLtsa.tsx index 0bffc87d5a..5630d40123 100644 --- a/source/frontend/src/hooks/pims-api/useApiLtsa.tsx +++ b/source/frontend/src/hooks/pims-api/useApiLtsa.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { LtsaOrders, ParcelInfoOrder, TitleSummary } from '@/interfaces/ltsaModels'; +import { LtsaOrders, ParcelInfoOrder, SpcpOrder, TitleSummary } from '@/interfaces/ltsaModels'; import useAxiosApi from './useApi'; @@ -18,6 +18,8 @@ export const useApiLtsa = () => { getParcelInfo: (pid: string) => api.post(`/tools/ltsa/order/parcelInfo?pid=${pid}`), getLtsaOrders: (pid: string) => api.post(`/tools/ltsa/all?pid=${pid}`), + getSPCPInfo: (strataPlanNumber: string) => + api.post(`/tools/ltsa/order/spcp?strataPlanNumber=${strataPlanNumber}`), }), [api], ); diff --git a/source/frontend/src/hooks/repositories/useComposedProperties.ts b/source/frontend/src/hooks/repositories/useComposedProperties.ts index d04ef62b78..e91ab642e9 100644 --- a/source/frontend/src/hooks/repositories/useComposedProperties.ts +++ b/source/frontend/src/hooks/repositories/useComposedProperties.ts @@ -4,13 +4,13 @@ import { LatLngLiteral } from 'leaflet'; import { useEffect, useMemo, useState } from 'react'; import { ComposedProperty } from '@/features/mapSideBar/property/ComposedProperty'; -import { LtsaOrders } from '@/interfaces/ltsaModels'; +import { LtsaOrders, SpcpOrder } from '@/interfaces/ltsaModels'; import { ApiGen_Concepts_Property } from '@/models/api/generated/ApiGen_Concepts_Property'; import { ApiGen_Concepts_PropertyAssociations } from '@/models/api/generated/ApiGen_Concepts_PropertyAssociations'; import { IBcAssessmentSummary } from '@/models/layers/bcAssesment'; import { TANTALIS_CrownLandTenures_Feature_Properties } from '@/models/layers/crownLand'; import { useTenant } from '@/tenants/useTenant'; -import { exists, isValidId } from '@/utils'; +import { exists, isPlanNumberSPCP, isValidId } from '@/utils'; import { useGeoServer } from '../layer-api/useGeoServer'; import { IWfsGetAllFeaturesOptions } from '../layer-api/useWfsLayer'; @@ -37,7 +37,11 @@ export default interface ComposedPropertyState { pid?: string; pin?: string; id?: number; + planNumber?: string; ltsaWrapper?: IResponseWrapper<(pid: string) => Promise>>; + spcpWrapper?: IResponseWrapper< + (strataPlanNumber: string) => Promise> + >; apiWrapper?: IResponseWrapper< (id: number) => Promise> >; @@ -64,6 +68,7 @@ export interface IUseComposedPropertiesProps { id?: number; pid?: number; pin?: number; + planNumber?: string; latLng?: LatLngLiteral; propertyTypes: PROPERTY_TYPES[]; } @@ -72,12 +77,13 @@ export const useComposedProperties = ({ id, pid, pin, + planNumber, latLng, propertyTypes, }: IUseComposedPropertiesProps): ComposedPropertyState => { const { getPropertyWrapper } = usePimsPropertyRepository(); const { getPropertyWfsWrapper } = useGeoServer(); - const getLtsaWrapper = useLtsa(); + const { ltsaRequestWrapper, getStrataPlanCommonProperty } = useLtsa(); const getPropertyAssociationsWrapper = usePropertyAssociations(); const { bcAssessment } = useTenant(); const { findByPid, findByPin, findByWrapper } = useFullyAttributedParcelMapLayer(); @@ -90,12 +96,16 @@ export const useComposedProperties = ({ const retrievedPid = getPropertyWrapper?.response?.pid?.toString() ?? pid?.toString(); const retrievedPin = getPropertyWrapper?.response?.pin?.toString() ?? pin?.toString(); + const retrievedPlanNumber = + getPropertyWrapper?.response?.planNumber?.toString() ?? planNumber?.toString(); const [composedProperty, setComposedProperty] = useState({ pid: undefined, pin: undefined, + planNumber: undefined, id: undefined, ltsaOrders: undefined, + spcpOrder: undefined, pimsProperty: undefined, propertyAssociations: undefined, parcelMapFeatureCollection: undefined, @@ -131,7 +141,8 @@ export const useComposedProperties = ({ typeCheckWrapper, ]); - const executeGetLtsa = getLtsaWrapper.execute; + const executeGetLtsa = ltsaRequestWrapper.execute; + const executeGetStrataLtsa = getStrataPlanCommonProperty.execute; const executeBcAssessmentSummary = getSummaryWrapper.execute; // calls to 3rd-party services (ie LTSA, ParcelMap, Tantalis Crown Land) @@ -148,6 +159,8 @@ export const useComposedProperties = ({ ); } else if (exists(retrievedPin)) { typeCheckWrapper(() => findByPin(retrievedPin, true), PROPERTY_TYPES.PARCEL_MAP); + } else if (exists(retrievedPlanNumber) && isPlanNumberSPCP(retrievedPlanNumber)) { + typeCheckWrapper(() => executeGetStrataLtsa(retrievedPlanNumber), PROPERTY_TYPES.LTSA); } // Crown land doesn't necessarily have a PIMS ID or PID or PIN so we need to use the lat/long of the selected property @@ -163,10 +176,12 @@ export const useComposedProperties = ({ executeGetLtsa, retrievedPid, retrievedPin, + retrievedPlanNumber, typeCheckWrapper, executeBcAssessmentSummary, findMultipleCrownLandTenure, latLng, + executeGetStrataLtsa, ]); useEffect(() => { @@ -174,7 +189,9 @@ export const useComposedProperties = ({ id: id, pid: retrievedPid, pin: retrievedPin, - ltsaOrders: getLtsaWrapper.response, + planNumber: retrievedPlanNumber, + ltsaOrders: ltsaRequestWrapper.response, + spcpOrder: getStrataPlanCommonProperty.response, pimsProperty: getPropertyWrapper.response, propertyAssociations: getPropertyAssociationsWrapper.response, parcelMapFeatureCollection: findByWrapper.response, @@ -187,13 +204,15 @@ export const useComposedProperties = ({ id, retrievedPid, retrievedPin, - getLtsaWrapper.response, getPropertyWrapper.response, getPropertyAssociationsWrapper.response, findByWrapper.response, getPropertyWfsWrapper.response, getSummaryWrapper.response, crownResponse, + retrievedPlanNumber, + ltsaRequestWrapper.response, + getStrataPlanCommonProperty.response, ]); return useMemo( @@ -201,17 +220,20 @@ export const useComposedProperties = ({ id: id, pid: pid?.toString() ?? retrievedPid, pin: pin?.toString() ?? retrievedPin, + planNumber: planNumber?.toString() ?? retrievedPlanNumber, composedProperty: composedProperty, - ltsaWrapper: getLtsaWrapper, + ltsaWrapper: ltsaRequestWrapper, + spcpWrapper: getStrataPlanCommonProperty, apiWrapper: getPropertyWrapper, propertyAssociationWrapper: getPropertyAssociationsWrapper, parcelMapWrapper: findByWrapper, geoserverWrapper: getPropertyWfsWrapper, bcAssessmentWrapper: getSummaryWrapper, composedLoading: - getLtsaWrapper?.loading || + ltsaRequestWrapper?.loading || getPropertyWrapper?.loading || getPropertyAssociationsWrapper?.loading || + getStrataPlanCommonProperty?.loading || findByWrapper?.loading || getPropertyWfsWrapper?.loading || getSummaryWrapper?.loading || @@ -223,8 +245,11 @@ export const useComposedProperties = ({ retrievedPid, pin, retrievedPin, + planNumber, + retrievedPlanNumber, composedProperty, - getLtsaWrapper, + ltsaRequestWrapper, + getStrataPlanCommonProperty, getPropertyWrapper, getPropertyAssociationsWrapper, findByWrapper, diff --git a/source/frontend/src/hooks/useDraftMarkerSynchronizer.ts b/source/frontend/src/hooks/useDraftMarkerSynchronizer.ts index 15757bf80e..1527170683 100644 --- a/source/frontend/src/hooks/useDraftMarkerSynchronizer.ts +++ b/source/frontend/src/hooks/useDraftMarkerSynchronizer.ts @@ -3,30 +3,14 @@ import { useCallback, useRef } from 'react'; import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; import { LocationBoundaryDataset } from '@/components/common/mapFSM/models'; -import { IMapProperty } from '@/components/propertySelector/models'; import useDeepCompareEffect from '@/hooks/util/useDeepCompareEffect'; import useIsMounted from '@/hooks/util/useIsMounted'; -import { latLngFromMapProperty } from '@/utils'; - -/** - * Get a list of file property markers from the current form values. - * As long as a parcel/building has both a lat and a lng it will be returned by this method. - * @param modifiedProperties the current form values to extract lat/lngs from. - */ -const getFilePropertyLocations = ( - modifiedProperties: IMapProperty[], -): LocationBoundaryDataset[] => { - return modifiedProperties.map((property: IMapProperty) => ({ - location: latLngFromMapProperty(property), - boundary: property?.polygon ?? null, - })); -}; /** * A hook that automatically syncs any updates to the lat/lngs of the parcel form with the map. * @param modifiedProperties array that contains the properties to be drawn. */ -const useDraftMarkerSynchronizer = (modifiedProperties: IMapProperty[]) => { +const useDraftMarkerSynchronizer = (modifiedProperties: LocationBoundaryDataset[]) => { const isMounted = useIsMounted(); const { setFilePropertyLocations } = useMapStateMachine(); @@ -36,9 +20,9 @@ const useDraftMarkerSynchronizer = (modifiedProperties: IMapProperty[]) => { * @param modifiedProperties the current properties */ const synchronizeMarkers = useCallback( - (modifiedProperties: IMapProperty[]) => { + (modifiedProperties: LocationBoundaryDataset[]) => { if (isMounted()) { - const filePropertyLocations = getFilePropertyLocations(modifiedProperties); + const filePropertyLocations = modifiedProperties; if (filePropertyLocations.length > 0) { setFilePropertyLocations(filePropertyLocations); } else { @@ -50,7 +34,7 @@ const useDraftMarkerSynchronizer = (modifiedProperties: IMapProperty[]) => { ); const synchronize = useRef( - debounce((modifiedProperties: IMapProperty[]) => { + debounce((modifiedProperties: LocationBoundaryDataset[]) => { synchronizeMarkers(modifiedProperties); }, 400), ).current; @@ -58,8 +42,6 @@ const useDraftMarkerSynchronizer = (modifiedProperties: IMapProperty[]) => { useDeepCompareEffect(() => { synchronize(modifiedProperties); }, [modifiedProperties, synchronize]); - - return; }; export default useDraftMarkerSynchronizer; diff --git a/source/frontend/src/hooks/useLtsa.ts b/source/frontend/src/hooks/useLtsa.ts index 18dccee8ba..15c2559585 100644 --- a/source/frontend/src/hooks/useLtsa.ts +++ b/source/frontend/src/hooks/useLtsa.ts @@ -1,7 +1,7 @@ import { AxiosResponse } from 'axios'; -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; -import { LtsaOrders } from '@/interfaces/ltsaModels'; +import { LtsaOrders, SpcpOrder } from '@/interfaces/ltsaModels'; import { pidFormatter } from '@/utils'; import { useApiLtsa } from './pims-api/useApiLtsa'; @@ -11,9 +11,9 @@ import { useApiRequestWrapper } from './util/useApiRequestWrapper'; * hook retrieves data from ltsa */ export const useLtsa = () => { - const { getLtsaOrders } = useApiLtsa(); + const { getLtsaOrders, getSPCPInfo } = useApiLtsa(); - const ltsaRequestWrapper = useApiRequestWrapper< + const ltsaRequestWrapperApi = useApiRequestWrapper< (pid: string) => Promise> >({ requestFunction: useCallback( @@ -23,5 +23,22 @@ export const useLtsa = () => { requestName: 'getLtsaData', }); - return ltsaRequestWrapper; + // Strata Plan Common Property + const ltsaStrataPlanCommonPropertyRequestApi = useApiRequestWrapper< + (planNumber: string) => Promise> + >({ + requestFunction: useCallback( + async (strataPlanNumber: string) => await getSPCPInfo(strataPlanNumber), + [getSPCPInfo], + ), + requestName: 'getLtsaData', + }); + + return useMemo( + () => ({ + ltsaRequestWrapper: ltsaRequestWrapperApi, + getStrataPlanCommonProperty: ltsaStrataPlanCommonPropertyRequestApi, + }), + [ltsaRequestWrapperApi, ltsaStrataPlanCommonPropertyRequestApi], + ); }; diff --git a/source/frontend/src/hooks/util/useApiRequestWrapper.ts b/source/frontend/src/hooks/util/useApiRequestWrapper.ts index 64f5354cfd..282c098f13 100644 --- a/source/frontend/src/hooks/util/useApiRequestWrapper.ts +++ b/source/frontend/src/hooks/util/useApiRequestWrapper.ts @@ -110,7 +110,7 @@ export const useApiRequestWrapper = < ); }); if (returnApiError) { - return axiosError.response.data; + return axiosError.response?.data ?? getApiError(axiosError); } } else if (!throwError) { // If no error handling is provided, fall back to the default PIMS error handler (bomb icon). @@ -122,7 +122,7 @@ export const useApiRequestWrapper = < }), ); if (returnApiError) { - return axiosError.response.data; + return axiosError.response.data ?? getApiError(axiosError); } } @@ -167,3 +167,12 @@ export const useApiRequestWrapper = < requestEndOn, }; }; + +const getApiError = (error: AxiosError): IApiError => ({ + details: error?.message, + error: + 'An unknown error occurred, contact your system administrator if you continue to see this error.', + errorCode: error?.code, + stackTrace: JSON.stringify(error), + type: 'Unknown', +}); diff --git a/source/frontend/src/mocks/acquisitionFiles.mock.ts b/source/frontend/src/mocks/acquisitionFiles.mock.ts index 417b90b02e..0129dac3d2 100644 --- a/source/frontend/src/mocks/acquisitionFiles.mock.ts +++ b/source/frontend/src/mocks/acquisitionFiles.mock.ts @@ -127,6 +127,7 @@ export const mockAcquisitionFileResponse = ( fileProperties: [ { id: 1, + isActive: null, propertyId: 292, property: { ...getMockApiProperty(), @@ -161,7 +162,7 @@ export const mockAcquisitionFileResponse = ( }, rowVersion: 1, }, - displayOrder: null, + displayOrder: 1, fileId: 1, file: null, propertyName: null, @@ -170,6 +171,7 @@ export const mockAcquisitionFileResponse = ( }, { id: 2, + isActive: null, propertyId: 443, property: { ...getMockApiProperty(), @@ -205,7 +207,7 @@ export const mockAcquisitionFileResponse = ( rowVersion: 1, }, rowVersion: 1, - displayOrder: null, + displayOrder: 2, fileId: 1, file: null, propertyName: null, diff --git a/source/frontend/src/mocks/compensations.mock.ts b/source/frontend/src/mocks/compensations.mock.ts index 79c833ae3a..faa1b3e859 100644 --- a/source/frontend/src/mocks/compensations.mock.ts +++ b/source/frontend/src/mocks/compensations.mock.ts @@ -154,6 +154,7 @@ export const getMockApiCompensationWithProperty = (): ApiGen_Concepts_Compensati propertyAcquisitionFileId: 2, acquisitionFileProperty: { id: 1, + isActive: null, file: null, propertyName: '', displayOrder: 0, @@ -177,6 +178,7 @@ export const getMockApiCompensationWithProperty = (): ApiGen_Concepts_Compensati propertyAcquisitionFileId: 2, acquisitionFileProperty: { id: 2, + isActive: null, file: null, propertyName: '', displayOrder: 0, @@ -434,6 +436,7 @@ export const getMockCompensationPropertiesReq = (): ApiGen_Concepts_AcquisitionF { file: null, id: 1, + isActive: null, propertyName: 'Property Test Name 1', location: { coordinate: { @@ -542,6 +545,7 @@ export const getMockCompensationPropertiesReq = (): ApiGen_Concepts_AcquisitionF { file: null, id: 26, + isActive: null, propertyName: 'Property Test Name 2', location: { coordinate: { diff --git a/source/frontend/src/mocks/fileProperty.mock.ts b/source/frontend/src/mocks/fileProperty.mock.ts index 04036df724..b1a04b1e33 100644 --- a/source/frontend/src/mocks/fileProperty.mock.ts +++ b/source/frontend/src/mocks/fileProperty.mock.ts @@ -5,6 +5,7 @@ import { getEmptyBaseAudit } from '@/models/defaultInitializers'; export const getEmptyFileProperty = (): ApiGen_Concepts_FileProperty => { return { id: 0, + isActive: null, propertyName: null, displayOrder: null, property: null, @@ -19,6 +20,7 @@ export const getEmptyFileProperty = (): ApiGen_Concepts_FileProperty => { export const getEmptyLeaseFileProperty = (): ApiGen_Concepts_PropertyLease => { const a: ApiGen_Concepts_PropertyLease = { file: undefined, + isActive: null, leaseArea: 0, areaUnitType: undefined, id: 0, diff --git a/source/frontend/src/mocks/lease.mock.ts b/source/frontend/src/mocks/lease.mock.ts index ee566d3b4d..e7c484054c 100644 --- a/source/frontend/src/mocks/lease.mock.ts +++ b/source/frontend/src/mocks/lease.mock.ts @@ -1674,6 +1674,7 @@ export const getMockLeaseStakeholders = (leaseId = 1): ApiGen_Concepts_LeaseStak export const getMockLeaseProperties = (leaseId = 1): ApiGen_Concepts_PropertyLease[] => [ { file: null, + isActive: null, leaseArea: 0, areaUnitType: null, id: 387, diff --git a/source/frontend/src/mocks/ltsa.mock.ts b/source/frontend/src/mocks/ltsa.mock.ts index d3c74254fc..400f01cf2e 100644 --- a/source/frontend/src/mocks/ltsa.mock.ts +++ b/source/frontend/src/mocks/ltsa.mock.ts @@ -1,4 +1,4 @@ -import { BillingInfo, LtsaOrders, OrderParent } from '@/interfaces/ltsaModels'; +import { BillingInfo, LtsaOrders, OrderParent, SpcpOrder } from '@/interfaces/ltsaModels'; export const getMockLtsaResponse: () => LtsaOrders = () => ({ parcelInfo: { @@ -190,3 +190,67 @@ export const getMockLtsaResponse: () => LtsaOrders = () => ({ }, ], }); + +export const getMockLtsaSPCPResponse: () => SpcpOrder = () => ({ + productType: OrderParent.ProductTypeEnum.CommonProperty, + fileReference: 'folio', + productOrderParameters: { + strataPlanNumber: 'VISXXXX', + includeCancelledInfo: false, + }, + orderId: 'XXXXXXXX-7c1c-449b-a214-157299732eb4', + status: OrderParent.StatusEnum.Processing, + orderedProduct: { + fieldedData: { + strataPlanIdentifier: { + strataPlanNumber: 'VISXXXX', + landTitleDistrict: 'VICTORIA', + }, + legalNotationsOnSCP: [ + { + legalNotationNumber: 'EP96596', + status: 'ACTIVE', + legalNotation: { + applicationReceivedDate: '2000-11-14T19:38:00Z', + originalLegalNotationNumber: 'EP96596', + legalNotationText: + 'THIS TITLE MAY BE AFFECTED BY A PERMIT UNDER PART 26 OF THE LOCAL\nGOVERNMENT ACT, SEE EP96596\n(AS TO ALL EXCEPT CLOSED ROAD ON PLAN VIP80164)\n', + }, + }, + ], + chargesOnSCP: [ + { + status: 'REGISTERED', + enteredDate: '2009-07-27T20:55:12Z', + interAlia: false, + chargeNumber: 'EC43157', + chargeRemarks: 'INTER ALIA\nDD EC43156, SECTION 47 LAND ACT\n', + charge: { + chargeNumber: 'EC43157', + transactionType: 'UNDERSURFACE AND OTHER EXC & RES', + applicationReceivedDate: '1989-05-08T18:18:00Z', + chargeOwnershipGroups: [ + { + jointTenancyIndication: false, + interestFractionNumerator: '1', + interestFractionDenominator: '1', + ownershipRemarks: '', + chargeOwners: [ + { + lastNameOrCorpName1: + 'HER MAJESTY THE QUEEN IN RIGHT OF THE PROVINCE OF BRITISH COLUMBIA', + incorporationNumber: '', + }, + ], + }, + ], + certificatesOfCharge: [], + correctionsAltos1: [], + corrections: [], + }, + chargeRelease: {}, + }, + ], + }, + }, +}); diff --git a/source/frontend/src/mocks/managementFiles.mock.ts b/source/frontend/src/mocks/managementFiles.mock.ts index 4dc30d60c2..cb046e7dde 100644 --- a/source/frontend/src/mocks/managementFiles.mock.ts +++ b/source/frontend/src/mocks/managementFiles.mock.ts @@ -68,6 +68,7 @@ export const mockManagementFileResponse = ( propertyName: null, location: null, displayOrder: null, + isActive: true, property: { id: 100, propertyType: null, @@ -297,6 +298,7 @@ export const mockManagementFilePropertiesResponse = (): ApiGen_Concepts_ManagementFileProperty[] => [ { id: 1, + isActive: null, propertyName: null, displayOrder: null, location: null, diff --git a/source/frontend/src/mocks/mapFSM.mock.ts b/source/frontend/src/mocks/mapFSM.mock.ts index 9550fa3f50..02ecac6473 100644 --- a/source/frontend/src/mocks/mapFSM.mock.ts +++ b/source/frontend/src/mocks/mapFSM.mock.ts @@ -91,4 +91,7 @@ export const mapMachineBaseMock: IMapStateMachineContext = { setMapLayersToRefresh: vi.fn(), setAdvancedSearchCriteria: vi.fn(), setCurrentMapBounds: vi.fn(), + mapMarkedLocation: undefined, + mapMarkLocation: vi.fn(), + mapClearLocationMark: vi.fn(), }; diff --git a/source/frontend/src/mocks/properties.mock.ts b/source/frontend/src/mocks/properties.mock.ts index bc2c72a272..42e42833aa 100644 --- a/source/frontend/src/mocks/properties.mock.ts +++ b/source/frontend/src/mocks/properties.mock.ts @@ -179,6 +179,7 @@ export const getMockApiPropertyFiles = (): ApiGen_Concepts_FileProperty[] => [ { id: 1, fileId: 1, + isActive: null, file: mockAcquisitionFileResponse(), propertyName: 'test property name', propertyId: 1, @@ -222,6 +223,7 @@ export const getMockApiPropertyFiles = (): ApiGen_Concepts_FileProperty[] => [ }, { id: 2, + isActive: null, propertyId: 2, fileId: 2, file: mockAcquisitionFileResponse(), @@ -278,6 +280,7 @@ export const getEmptyPropertyLease = (): ApiGen_Concepts_PropertyLease => { property: null, propertyId: 0, location: null, + isActive: null, rowVersion: null, }; }; diff --git a/source/frontend/src/mocks/researchFile.mock.ts b/source/frontend/src/mocks/researchFile.mock.ts index 375b53ac80..739b40c587 100644 --- a/source/frontend/src/mocks/researchFile.mock.ts +++ b/source/frontend/src/mocks/researchFile.mock.ts @@ -20,6 +20,7 @@ export const getMockResearchFile = (): ApiGen_Concepts_ResearchFile => ({ fileProperties: [ { id: 55, + isActive: null, propertyId: 495, property: { ...getMockApiProperty(), @@ -150,6 +151,7 @@ export const getEmptyResearchFileProperty = (): ApiGen_Concepts_ResearchFileProp propertyResearchPurposeTypes: null, researchSummary: null, location: null, + isActive: null, ...getEmptyBaseAudit(), }); @@ -161,7 +163,7 @@ export const getMockResearchFileProperty = ( fileId, propertyName: 'Corner of Nakya PL', propertyId: 495, - + isActive: null, propertyResearchPurposeTypes: [ { id: 22, diff --git a/source/frontend/src/models/api/generated/ApiGen_Concepts_FileProperty.ts b/source/frontend/src/models/api/generated/ApiGen_Concepts_FileProperty.ts index 05e4a9862a..dbb0fbcc5e 100644 --- a/source/frontend/src/models/api/generated/ApiGen_Concepts_FileProperty.ts +++ b/source/frontend/src/models/api/generated/ApiGen_Concepts_FileProperty.ts @@ -15,6 +15,7 @@ export interface ApiGen_Concepts_FileProperty extends ApiGen_Base_BaseConcurrent location: ApiGen_Concepts_Geometry | null; displayOrder: number | null; property: ApiGen_Concepts_Property | null; + isActive: boolean | null; propertyId: number; file: ApiGen_Concepts_File | null; } diff --git a/source/frontend/src/utils/mapPropertyUtils.test.tsx b/source/frontend/src/utils/mapPropertyUtils.test.tsx index a6d4607f2f..aa3941e29e 100644 --- a/source/frontend/src/utils/mapPropertyUtils.test.tsx +++ b/source/frontend/src/utils/mapPropertyUtils.test.tsx @@ -366,7 +366,7 @@ describe('mapPropertyUtils', () => { it.each([ [ { ...getEmptyFileProperty(), location: getMockLocation() }, - { location: getMockLatLng(), boundary: null }, + { location: getMockLatLng(), boundary: null, isActive: null }, ], [ { @@ -378,7 +378,7 @@ describe('mapPropertyUtils', () => { boundary: getMockPolygon(), }, }, - { location: getMockLatLng(), boundary: getMockPolygon() }, + { location: getMockLatLng(), boundary: getMockPolygon(), isActive: null }, ], [{ ...getEmptyFileProperty(), location: null }, null], ])( diff --git a/source/frontend/src/utils/mapPropertyUtils.ts b/source/frontend/src/utils/mapPropertyUtils.ts index 45b203c9c8..5bc0a5db78 100644 --- a/source/frontend/src/utils/mapPropertyUtils.ts +++ b/source/frontend/src/utils/mapPropertyUtils.ts @@ -5,10 +5,11 @@ import { GeoJsonProperties, Geometry, MultiPolygon, + Point, Polygon, } from 'geojson'; import { geoJSON, LatLngLiteral } from 'leaflet'; -import { compact, isNumber, orderBy } from 'lodash'; +import { chain, compact, isNumber } from 'lodash'; import polylabel from 'polylabel'; import { toast } from 'react-toastify'; @@ -272,6 +273,16 @@ export function featuresetToMapProperty( } } +export const featuresetToLocationBoundaryDataset = ( + featureSet: SelectedFeatureDataset, +): LocationBoundaryDataset => { + return { + location: featureSet?.fileLocation ?? featureSet?.location, + boundary: featureSet?.pimsFeature?.geometry ?? featureSet?.parcelFeature?.geometry ?? null, + isActive: featureSet.isActive, + }; +}; + export function pidFromFeatureSet(featureset: SelectedFeatureDataset): string | null { if (exists(featureset.pimsFeature?.properties)) { return exists(featureset.pimsFeature?.properties?.PID) @@ -302,6 +313,16 @@ export function locationFromFileProperty( return fileProperty?.location ?? fileProperty?.property?.location ?? null; } +export function boundaryFromFileProperty( + fileProperty: ApiGen_Concepts_FileProperty | undefined | null, +): Geometry | null { + return ( + fileProperty?.property?.boundary ?? + pimsGeomeryToGeometry(fileProperty?.property?.location) ?? + null + ); +} + export function latLngFromMapProperty( mapProperty: IMapProperty | undefined | null, ): LatLngLiteral | null { @@ -311,12 +332,46 @@ export function latLngFromMapProperty( }; } +export function latLngLiteralToGeometry(latLng: LatLngLiteral | null | undefined): Point | null { + if (exists(latLng)) { + return { type: 'Point', coordinates: [latLng.lng, latLng.lat] }; + } + return null; +} + +export function pimsGeomeryToGeometry( + pimsGeomery: ApiGen_Concepts_Geometry | null | undefined, +): Point | null { + if (exists(pimsGeomery?.coordinate)) { + return { type: 'Point', coordinates: [pimsGeomery.coordinate.x, pimsGeomery.coordinate.y] }; + } + return null; +} + export function filePropertyToLocationBoundaryDataset( fileProperty: ApiGen_Concepts_FileProperty | undefined | null, ): LocationBoundaryDataset | null { const geom = locationFromFileProperty(fileProperty); const location = getLatLng(geom); - return exists(location) ? { location, boundary: fileProperty?.property?.boundary ?? null } : null; + return exists(location) + ? { + location, + boundary: fileProperty?.property?.boundary ?? null, + isActive: fileProperty.isActive, + } + : null; +} + +export function propertyToLocationBoundaryDataset( + property: ApiGen_Concepts_Property | undefined | null, +): LocationBoundaryDataset | null { + const location = getLatLng(property.location); + return exists(location) + ? { + location, + boundary: property?.boundary ?? null, + } + : null; } /** @@ -359,8 +414,9 @@ export function sortFileProperties( fileProperties: T[] | null, ): T[] | null { if (exists(fileProperties)) { - const sortedProperties = orderBy(fileProperties, fp => fp.displayOrder ?? Infinity, 'asc'); - return sortedProperties; + return chain(fileProperties) + .orderBy([fp => fp.displayOrder ?? Infinity], ['asc']) + .value(); } return null; } diff --git a/source/frontend/src/utils/propertyUtils.ts b/source/frontend/src/utils/propertyUtils.ts index 924d152765..cb4b0e7b4e 100644 --- a/source/frontend/src/utils/propertyUtils.ts +++ b/source/frontend/src/utils/propertyUtils.ts @@ -122,6 +122,17 @@ export function formatApiPropertyManagementLease( } } +export function isPlanNumberSPCP(planNumber: string): boolean { + if (!exists(planNumber)) { + return false; + } + + const nonNumericPrefix = firstOrNull(planNumber?.match(/^\D+/)); // Extract non-numeric prefix + const isStrataCommonPropertyPrefix = nonNumericPrefix?.toUpperCase()?.endsWith('S'); // Check if the last character is 'S' PSP-10455 + + return isStrataCommonPropertyPrefix; +} + export function isStrataCommonProperty( feature: Feature | undefined | null, ) { @@ -130,12 +141,11 @@ export function isStrataCommonProperty( } const planNumber = feature.properties.PLAN_NUMBER; - const nonNumericPrefix = firstOrNull(planNumber?.match(/^\D+/)); // Extract non-numeric prefix - const isStrataCommonPropertyPrefix = nonNumericPrefix?.toUpperCase()?.endsWith('S'); // Check if the last character is 'S' PSP-10455 + return ( + isPlanNumberSPCP(planNumber) && feature.properties.PID === null && feature.properties.PIN === null && - isStrataCommonPropertyPrefix && feature.properties.OWNER_TYPE === 'Unclassified' ); } diff --git a/tools/geoserver/geoserver_data/psp/mssql/PIMS_PROPERTY_BOUNDARY_RESEARCH_VW/layer.xml b/tools/geoserver/geoserver_data/psp/mssql/PIMS_PROPERTY_BOUNDARY_RESEARCH_VW/layer.xml index af6c3b968e..5873e38d9f 100644 --- a/tools/geoserver/geoserver_data/psp/mssql/PIMS_PROPERTY_BOUNDARY_RESEARCH_VW/layer.xml +++ b/tools/geoserver/geoserver_data/psp/mssql/PIMS_PROPERTY_BOUNDARY_RESEARCH_VW/layer.xml @@ -13,5 +13,5 @@ 0 2025-06-20 19:45:48.732 UTC - 2025-06-20 19:52:33.270 UTC + 2025-06-25 22:24:17.936 UTC \ No newline at end of file