From e9a3540ab81f474f6f0e8c54b4b1e6515eea41b0 Mon Sep 17 00:00:00 2001 From: devinleighsmith Date: Tue, 17 Sep 2024 00:44:48 -0700 Subject: [PATCH 1/4] psp-9148 Add consultation outcomes and business logic. --- .../api/Controllers/LookupController.cs | 2 + source/backend/api/Services/LeaseService.cs | 2 +- .../CodeTypes/ConsultationOutcomeTypes.cs | 24 + .../Concepts/Lease/ConsultationLeaseMap.cs | 2 + .../Concepts/Lease/ConsultationLeaseModel.cs | 2 + .../Repositories/ConsultationRepository.cs | 2 + .../Interfaces/ILookupRepository.cs | 2 + .../dal/Repositories/LookupRepository.cs | 5 + .../Partials/ConsultationOutcomeTypeCode.cs | 33 + source/backend/entities/PimsBaseContext.cs | 57 +- .../ef/PimsConsultationOutcomeType.cs | 79 ++ .../entities/ef/PimsLeaseConsultation.cs | 13 + .../entities/ef/PimsLeaseConsultationHist.cs | 5 + .../entities/ef/PimsPropertyBoundaryLiteVw.cs | 36 + .../entities/ef/PimsPropertyLocationLiteVw.cs | 36 + .../entities/ef/PimsResearchFileNote.cs | 2 - .../core/Entities/LeaseConsultationHelper.cs | 55 + .../Leases/ConsultationControllerTest.cs | 204 +++ .../unit/api/Services/LeaseServiceTest.cs | 170 +++ .../ConsultationRepositoryTest.cs | 151 +++ source/frontend/src/constants/API.ts | 1 + .../AddLeaseStakeholderForm.test.tsx | 25 +- .../detail/ConsultationListContainer.test.tsx | 164 +++ .../detail/ConsultationListContainer.tsx | 4 +- .../detail/ConsultationListView.test.tsx | 171 +++ .../detail/ConsultationListView.tsx | 6 +- .../ConsultationListContainer.test.tsx.snap | 10 + .../ConsultationListView.test.tsx.snap | 1124 +++++++++++++++++ .../edit/ConsultationAddContainer.test.tsx | 149 +++ .../edit/ConsultationEditForm.test.tsx | 198 +++ .../edit/ConsultationEditForm.tsx | 26 + .../edit/ConsultationUpdateContainer.test.tsx | 208 +++ .../edit/ConsultationUpdateContainer.tsx | 4 +- .../edit/EditConsultationYupSchema.ts | 14 +- .../ConsultationAddContainer.test.tsx.snap | 10 + .../ConsultationEditForm.test.tsx.snap | 867 +++++++++++++ .../ConsultationUpdateContainer.test.tsx.snap | 10 + .../lease/tabs/consultations/edit/models.ts | 8 + .../frontend/src/mocks/consultations.mock.ts | 46 + source/frontend/src/mocks/lookups.mock.ts | 35 + ...iGen_CodeTypes_ConsultationOutcomeTypes.ts | 11 + .../ApiGen_Concepts_ConsultationLease.ts | 1 + .../ApiGen_Concepts_LeaseStakeholderType.ts | 7 +- 43 files changed, 3947 insertions(+), 34 deletions(-) create mode 100644 source/backend/apimodels/CodeTypes/ConsultationOutcomeTypes.cs create mode 100644 source/backend/entities/Partials/ConsultationOutcomeTypeCode.cs create mode 100644 source/backend/entities/ef/PimsConsultationOutcomeType.cs create mode 100644 source/backend/entities/ef/PimsPropertyBoundaryLiteVw.cs create mode 100644 source/backend/entities/ef/PimsPropertyLocationLiteVw.cs create mode 100644 source/backend/tests/core/Entities/LeaseConsultationHelper.cs create mode 100644 source/backend/tests/unit/api/Controllers/Leases/ConsultationControllerTest.cs create mode 100644 source/backend/tests/unit/dal/Repositories/ConsultationRepositoryTest.cs create mode 100644 source/frontend/src/features/mapSideBar/lease/tabs/consultations/detail/ConsultationListContainer.test.tsx create mode 100644 source/frontend/src/features/mapSideBar/lease/tabs/consultations/detail/ConsultationListView.test.tsx create mode 100644 source/frontend/src/features/mapSideBar/lease/tabs/consultations/detail/__snapshots__/ConsultationListContainer.test.tsx.snap create mode 100644 source/frontend/src/features/mapSideBar/lease/tabs/consultations/detail/__snapshots__/ConsultationListView.test.tsx.snap create mode 100644 source/frontend/src/features/mapSideBar/lease/tabs/consultations/edit/ConsultationAddContainer.test.tsx create mode 100644 source/frontend/src/features/mapSideBar/lease/tabs/consultations/edit/ConsultationEditForm.test.tsx create mode 100644 source/frontend/src/features/mapSideBar/lease/tabs/consultations/edit/ConsultationUpdateContainer.test.tsx create mode 100644 source/frontend/src/features/mapSideBar/lease/tabs/consultations/edit/__snapshots__/ConsultationAddContainer.test.tsx.snap create mode 100644 source/frontend/src/features/mapSideBar/lease/tabs/consultations/edit/__snapshots__/ConsultationEditForm.test.tsx.snap create mode 100644 source/frontend/src/features/mapSideBar/lease/tabs/consultations/edit/__snapshots__/ConsultationUpdateContainer.test.tsx.snap create mode 100644 source/frontend/src/mocks/consultations.mock.ts create mode 100644 source/frontend/src/models/api/generated/ApiGen_CodeTypes_ConsultationOutcomeTypes.ts diff --git a/source/backend/api/Controllers/LookupController.cs b/source/backend/api/Controllers/LookupController.cs index ba37472735..0e711f9a6c 100644 --- a/source/backend/api/Controllers/LookupController.cs +++ b/source/backend/api/Controllers/LookupController.cs @@ -146,6 +146,7 @@ public IActionResult GetAll() var historicalNumberTypes = _mapper.Map(_lookupRepository.GetAllHistoricalNumberTypes()); var leaseChecklistSectionTypes = _mapper.Map(_lookupRepository.GetAllLeaseChecklistSectionTypes()); var leasePaymentCategoryTypes = _mapper.Map(_lookupRepository.GetAllLeasePaymentCategoryTypes()); + var consultationOutcomeTypes = _mapper.Map(_lookupRepository.GetAllConsultationOutcomeTypes()); var codes = new List(); codes.AddRange(areaUnitTypes); @@ -220,6 +221,7 @@ public IActionResult GetAll() codes.AddRange(historicalNumberTypes); codes.AddRange(leaseChecklistSectionTypes); codes.AddRange(leasePaymentCategoryTypes); + codes.AddRange(consultationOutcomeTypes); var response = new JsonResult(codes); diff --git a/source/backend/api/Services/LeaseService.cs b/source/backend/api/Services/LeaseService.cs index 80954d840c..b0f56bb037 100644 --- a/source/backend/api/Services/LeaseService.cs +++ b/source/backend/api/Services/LeaseService.cs @@ -371,7 +371,7 @@ public IEnumerable GetConsultations(long leaseId) public PimsLeaseConsultation GetConsultationById(long consultationId) { _logger.LogInformation("Getting consultation with id: {consultationId}", consultationId); - _user.ThrowIfNotAuthorized(Permissions.LeaseEdit); + _user.ThrowIfNotAuthorized(Permissions.LeaseView); return _consultationRepository.GetConsultationById(consultationId); } diff --git a/source/backend/apimodels/CodeTypes/ConsultationOutcomeTypes.cs b/source/backend/apimodels/CodeTypes/ConsultationOutcomeTypes.cs new file mode 100644 index 0000000000..21a9b179ef --- /dev/null +++ b/source/backend/apimodels/CodeTypes/ConsultationOutcomeTypes.cs @@ -0,0 +1,24 @@ +using System.Runtime.Serialization; +using System.Text.Json.Serialization; + +namespace Pims.Api.Models.CodeTypes +{ + [JsonConverter(typeof(JsonStringEnumMemberConverter))] + public enum ConsultationOutcomeTypes + { + [EnumMember(Value = "APPRDENIED")] + APPRDENIED, + + [EnumMember(Value = "APPRGRANTED")] + APPRGRANTED, + + [EnumMember(Value = "CONSCOMPLTD")] + CONSCOMPLTD, + + [EnumMember(Value = "CONSDISCONT")] + CONSDISCONT, + + [EnumMember(Value = "INPROGRESS")] + INPROGRESS, + } +} diff --git a/source/backend/apimodels/Models/Concepts/Lease/ConsultationLeaseMap.cs b/source/backend/apimodels/Models/Concepts/Lease/ConsultationLeaseMap.cs index 43198aee82..dfb033738d 100644 --- a/source/backend/apimodels/Models/Concepts/Lease/ConsultationLeaseMap.cs +++ b/source/backend/apimodels/Models/Concepts/Lease/ConsultationLeaseMap.cs @@ -21,6 +21,7 @@ public void Register(TypeAdapterConfig config) .Map(dest => dest.PrimaryContact, src => src.PrimaryContact) .Map(dest => dest.ConsultationTypeCode, src => src.ConsultationTypeCodeNavigation) .Map(dest => dest.ConsultationStatusTypeCode, src => src.ConsultationStatusTypeCodeNavigation) + .Map(dest => dest.ConsultationOutcomeTypeCode, src => src.ConsultationOutcomeTypeCodeNavigation) .Map(dest => dest.OtherDescription, src => src.OtherDescription) .Map(dest => dest.RequestedOn, src => src.RequestedOn.ToNullableDateOnly()) .Map(dest => dest.IsResponseReceived, src => src.IsResponseReceived) @@ -38,6 +39,7 @@ public void Register(TypeAdapterConfig config) .Map(dest => dest.PrimaryContact, src => src.PrimaryContact) .Map(dest => dest.ConsultationTypeCode, src => src.ConsultationTypeCode.Id) .Map(dest => dest.ConsultationStatusTypeCode, src => src.ConsultationStatusTypeCode.Id) + .Map(dest => dest.ConsultationOutcomeTypeCode, src => src.ConsultationOutcomeTypeCode.Id) .Map(dest => dest.OtherDescription, src => src.OtherDescription) .Map(dest => dest.RequestedOn, src => src.RequestedOn.ToNullableDateTime()) .Map(dest => dest.IsResponseReceived, src => src.IsResponseReceived) diff --git a/source/backend/apimodels/Models/Concepts/Lease/ConsultationLeaseModel.cs b/source/backend/apimodels/Models/Concepts/Lease/ConsultationLeaseModel.cs index 1038a29860..177069ca58 100644 --- a/source/backend/apimodels/Models/Concepts/Lease/ConsultationLeaseModel.cs +++ b/source/backend/apimodels/Models/Concepts/Lease/ConsultationLeaseModel.cs @@ -31,6 +31,8 @@ public class ConsultationLeaseModel : BaseAuditModel public CodeTypeModel ConsultationStatusTypeCode { get; set; } + public CodeTypeModel ConsultationOutcomeTypeCode { get; set; } + public string OtherDescription { get; set; } public DateOnly? RequestedOn { get; set; } diff --git a/source/backend/dal/Repositories/ConsultationRepository.cs b/source/backend/dal/Repositories/ConsultationRepository.cs index a40d99a95a..612737b0b7 100644 --- a/source/backend/dal/Repositories/ConsultationRepository.cs +++ b/source/backend/dal/Repositories/ConsultationRepository.cs @@ -38,6 +38,7 @@ public List GetConsultationsByLease(long leaseId) return Context.PimsLeaseConsultations .Where(lc => lc.LeaseId == leaseId) .Include(lc => lc.ConsultationTypeCodeNavigation) + .Include(lc => lc.ConsultationOutcomeTypeCodeNavigation) .AsNoTracking() .ToList(); } @@ -58,6 +59,7 @@ public PimsLeaseConsultation GetConsultationById(long consultationId) return Context.PimsLeaseConsultations.Where(x => x.LeaseConsultationId == consultationId) .AsNoTracking() .Include(x => x.ConsultationTypeCodeNavigation) + .Include(x => x.ConsultationOutcomeTypeCodeNavigation) .FirstOrDefault() ?? throw new KeyNotFoundException(); } diff --git a/source/backend/dal/Repositories/Interfaces/ILookupRepository.cs b/source/backend/dal/Repositories/Interfaces/ILookupRepository.cs index 353aa2112c..a17c8a98cf 100644 --- a/source/backend/dal/Repositories/Interfaces/ILookupRepository.cs +++ b/source/backend/dal/Repositories/Interfaces/ILookupRepository.cs @@ -155,5 +155,7 @@ public interface ILookupRepository : IRepository IEnumerable GetAllLeaseChecklistSectionTypes(); IEnumerable GetAllLeasePaymentCategoryTypes(); + + IEnumerable GetAllConsultationOutcomeTypes(); } } diff --git a/source/backend/dal/Repositories/LookupRepository.cs b/source/backend/dal/Repositories/LookupRepository.cs index fdeaac0d7d..644c2c56db 100644 --- a/source/backend/dal/Repositories/LookupRepository.cs +++ b/source/backend/dal/Repositories/LookupRepository.cs @@ -435,6 +435,11 @@ public IEnumerable GetAllLeasePaymentCategoryTypes return Context.PimsLeasePaymentCategoryTypes.AsNoTracking().OrderBy(a => a.DisplayOrder).ToArray(); } + public IEnumerable GetAllConsultationOutcomeTypes() + { + return Context.PimsConsultationOutcomeTypes.AsNoTracking().OrderBy(a => a.DisplayOrder).ToArray(); + } + #endregion } } diff --git a/source/backend/entities/Partials/ConsultationOutcomeTypeCode.cs b/source/backend/entities/Partials/ConsultationOutcomeTypeCode.cs new file mode 100644 index 0000000000..ffefd9aebe --- /dev/null +++ b/source/backend/entities/Partials/ConsultationOutcomeTypeCode.cs @@ -0,0 +1,33 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace Pims.Dal.Entities +{ + /// + /// PimsConsultationOutcomeType class, provides an entity for the datamodel to manage Consultation outcome types. + /// + public partial class PimsConsultationOutcomeType : ITypeEntity + { + #region Properties + + /// + /// get/set - Primary key to identify Consultation outcome type. + /// + [NotMapped] + public string Id { get => ConsultationOutcomeTypeCode; set => ConsultationOutcomeTypeCode = value; } + #endregion + + #region Constructors + + public PimsConsultationOutcomeType() { } + + /// + /// Create a new instance of a PimsConsultationOutcomeType class. + /// + /// + public PimsConsultationOutcomeType(string id) + { + Id = id; + } + #endregion + } +} diff --git a/source/backend/entities/PimsBaseContext.cs b/source/backend/entities/PimsBaseContext.cs index 44f2deb602..58344f1110 100644 --- a/source/backend/entities/PimsBaseContext.cs +++ b/source/backend/entities/PimsBaseContext.cs @@ -102,6 +102,8 @@ public PimsBaseContext(DbContextOptions options) public virtual DbSet PimsCompensationRequisitionHists { get; set; } + public virtual DbSet PimsConsultationOutcomeTypes { get; set; } + public virtual DbSet PimsConsultationStatusTypes { get; set; } public virtual DbSet PimsConsultationTypes { get; set; } @@ -462,6 +464,8 @@ public PimsBaseContext(DbContextOptions options) public virtual DbSet PimsPropertyAnomalyTypes { get; set; } + public virtual DbSet PimsPropertyBoundaryLiteVws { get; set; } + public virtual DbSet PimsPropertyBoundaryVws { get; set; } public virtual DbSet PimsPropertyContacts { get; set; } @@ -480,6 +484,8 @@ public PimsBaseContext(DbContextOptions options) public virtual DbSet PimsPropertyLeaseHists { get; set; } + public virtual DbSet PimsPropertyLocationLiteVws { get; set; } + public virtual DbSet PimsPropertyLocationVws { get; set; } public virtual DbSet PimsPropertyOperations { get; set; } @@ -1698,6 +1704,38 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.Property(e => e.EffectiveDateHist).HasDefaultValueSql("(getutcdate())"); }); + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.ConsultationOutcomeTypeCode).HasName("OUTCMT_PK"); + + entity.ToTable("PIMS_CONSULTATION_OUTCOME_TYPE", tb => + { + tb.HasComment("Description of the consultation outcome type for a lease or license."); + tb.HasTrigger("PIMS_OUTCMT_I_S_I_TR"); + tb.HasTrigger("PIMS_OUTCMT_I_S_U_TR"); + }); + + entity.Property(e => e.ConsultationOutcomeTypeCode).HasComment("Code value of the consultation outcome type."); + entity.Property(e => e.ConcurrencyControlNumber) + .HasDefaultValue(1L) + .HasComment("Application code is responsible for retrieving the row and then incrementing the value of the CONCURRENCY_CONTROL_NUMBER column by one prior to issuing an update. If this is done then the update will succeed, provided that the row was not updated by any o"); + entity.Property(e => e.DbCreateTimestamp) + .HasDefaultValueSql("(getutcdate())") + .HasComment("The date and time the record was created."); + entity.Property(e => e.DbCreateUserid) + .HasDefaultValueSql("(user_name())") + .HasComment("The user or proxy account that created the record."); + entity.Property(e => e.DbLastUpdateTimestamp) + .HasDefaultValueSql("(getutcdate())") + .HasComment("The date and time the record was created or last updated."); + entity.Property(e => e.DbLastUpdateUserid) + .HasDefaultValueSql("(user_name())") + .HasComment("The user or proxy account that created or last updated the record."); + entity.Property(e => e.Description).HasComment("Description of the consultation outcome type."); + entity.Property(e => e.DisplayOrder).HasComment("Onscreen display order of the code types."); + entity.Property(e => e.IsDisabled).HasComment("Indicates if the code type is active."); + }); + modelBuilder.Entity(entity => { entity.HasKey(e => e.ConsultationStatusTypeCode).HasName("CONSTY_PK"); @@ -4345,6 +4383,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.Property(e => e.ConcurrencyControlNumber) .HasDefaultValue(1L) .HasComment("Application code is responsible for retrieving the row and then incrementing the value of the CONCURRENCY_CONTROL_NUMBER column by one prior to issuing an update. If this is done then the update will succeed, provided that the row was not updated by any o"); + entity.Property(e => e.ConsultationOutcomeTypeCode) + .HasDefaultValue("INPROGRESS") + .HasComment("Foreign key to the PIMS_CONSULTATION_OUTCOME_TYPE table."); entity.Property(e => e.ConsultationStatusTypeCode) .HasDefaultValue("UNKNOWN") .HasComment("Foreign key to the PIMS_CONSULTATION_STATUS_TYPE table."); @@ -4373,6 +4414,10 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.Property(e => e.RequestedOn).HasComment("Date that the approval / consultation request was sent."); entity.Property(e => e.ResponseReceivedDate).HasComment("Date that the consultation request response was received."); + entity.HasOne(d => d.ConsultationOutcomeTypeCodeNavigation).WithMany(p => p.PimsLeaseConsultations) + .OnDelete(DeleteBehavior.ClientSetNull) + .HasConstraintName("PIM_OUTCMT_PIM_LESCON_FK"); + entity.HasOne(d => d.ConsultationStatusTypeCodeNavigation).WithMany(p => p.PimsLeaseConsultations) .OnDelete(DeleteBehavior.ClientSetNull) .HasConstraintName("PIM_CONSTY_PIM_LESCON_FK"); @@ -6845,6 +6890,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.Property(e => e.IsDisabled).HasComment("Indicates if the code is disabled."); }); + modelBuilder.Entity(entity => + { + entity.ToView("PIMS_PROPERTY_BOUNDARY_LITE_VW"); + }); + modelBuilder.Entity(entity => { entity.ToView("PIMS_PROPERTY_BOUNDARY_VW"); @@ -7019,6 +7069,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.Property(e => e.EffectiveDateHist).HasDefaultValueSql("(getutcdate())"); }); + modelBuilder.Entity(entity => + { + entity.ToView("PIMS_PROPERTY_LOCATION_LITE_VW"); + }); + modelBuilder.Entity(entity => { entity.ToView("PIMS_PROPERTY_LOCATION_VW"); @@ -7538,7 +7593,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity(entity => { - entity.HasKey(e => new { e.ResearchFileNoteId, e.ResearchFileId }).HasName("RFLNOT_PK"); + entity.HasKey(e => e.ResearchFileNoteId).HasName("RFLNOT_PK"); entity.ToTable("PIMS_RESEARCH_FILE_NOTE", tb => { diff --git a/source/backend/entities/ef/PimsConsultationOutcomeType.cs b/source/backend/entities/ef/PimsConsultationOutcomeType.cs new file mode 100644 index 0000000000..1153c3dd73 --- /dev/null +++ b/source/backend/entities/ef/PimsConsultationOutcomeType.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.EntityFrameworkCore; + +namespace Pims.Dal.Entities; + +/// +/// Description of the consultation outcome type for a lease or license. +/// +[Table("PIMS_CONSULTATION_OUTCOME_TYPE")] +public partial class PimsConsultationOutcomeType +{ + /// + /// Code value of the consultation outcome type. + /// + [Key] + [Column("CONSULTATION_OUTCOME_TYPE_CODE")] + [StringLength(20)] + public string ConsultationOutcomeTypeCode { get; set; } + + /// + /// Description of the consultation outcome type. + /// + [Required] + [Column("DESCRIPTION")] + [StringLength(200)] + public string Description { get; set; } + + /// + /// Onscreen display order of the code types. + /// + [Column("DISPLAY_ORDER")] + public int? DisplayOrder { get; set; } + + /// + /// Indicates if the code type is active. + /// + [Column("IS_DISABLED")] + public bool IsDisabled { get; set; } + + /// + /// Application code is responsible for retrieving the row and then incrementing the value of the CONCURRENCY_CONTROL_NUMBER column by one prior to issuing an update. If this is done then the update will succeed, provided that the row was not updated by any o + /// + [Column("CONCURRENCY_CONTROL_NUMBER")] + public long ConcurrencyControlNumber { get; set; } + + /// + /// The date and time the record was created. + /// + [Column("DB_CREATE_TIMESTAMP", TypeName = "datetime")] + public DateTime DbCreateTimestamp { get; set; } + + /// + /// The user or proxy account that created the record. + /// + [Required] + [Column("DB_CREATE_USERID")] + [StringLength(30)] + public string DbCreateUserid { get; set; } + + /// + /// The date and time the record was created or last updated. + /// + [Column("DB_LAST_UPDATE_TIMESTAMP", TypeName = "datetime")] + public DateTime DbLastUpdateTimestamp { get; set; } + + /// + /// The user or proxy account that created or last updated the record. + /// + [Required] + [Column("DB_LAST_UPDATE_USERID")] + [StringLength(30)] + public string DbLastUpdateUserid { get; set; } + + [InverseProperty("ConsultationOutcomeTypeCodeNavigation")] + public virtual ICollection PimsLeaseConsultations { get; set; } = new List(); +} diff --git a/source/backend/entities/ef/PimsLeaseConsultation.cs b/source/backend/entities/ef/PimsLeaseConsultation.cs index fbf1ebe80b..8f2abaa7bc 100644 --- a/source/backend/entities/ef/PimsLeaseConsultation.cs +++ b/source/backend/entities/ef/PimsLeaseConsultation.cs @@ -7,6 +7,7 @@ namespace Pims.Dal.Entities; [Table("PIMS_LEASE_CONSULTATION")] +[Index("ConsultationOutcomeTypeCode", Name = "LESCON_CONSULTATION_OUTCOME_TYPE_CODE_IDX")] [Index("ConsultationStatusTypeCode", Name = "LESCON_CONSULTATION_STATUS_TYPE_CODE_IDX")] [Index("ConsultationTypeCode", Name = "LESCON_CONSULTATION_TYPE_CODE_IDX")] [Index("LeaseId", Name = "LESCON_LEASE_ID_IDX")] @@ -62,6 +63,14 @@ public partial class PimsLeaseConsultation [StringLength(20)] public string ConsultationStatusTypeCode { get; set; } + /// + /// Foreign key to the PIMS_CONSULTATION_OUTCOME_TYPE table. + /// + [Required] + [Column("CONSULTATION_OUTCOME_TYPE_CODE")] + [StringLength(20)] + public string ConsultationOutcomeTypeCode { get; set; } + /// /// Description for the approval / consultation when "Other" consultation type is selected. /// @@ -189,6 +198,10 @@ public partial class PimsLeaseConsultation [StringLength(30)] public string DbLastUpdateUserid { get; set; } + [ForeignKey("ConsultationOutcomeTypeCode")] + [InverseProperty("PimsLeaseConsultations")] + public virtual PimsConsultationOutcomeType ConsultationOutcomeTypeCodeNavigation { get; set; } + [ForeignKey("ConsultationStatusTypeCode")] [InverseProperty("PimsLeaseConsultations")] public virtual PimsConsultationStatusType ConsultationStatusTypeCodeNavigation { get; set; } diff --git a/source/backend/entities/ef/PimsLeaseConsultationHist.cs b/source/backend/entities/ef/PimsLeaseConsultationHist.cs index 8e7b217888..41fdefa880 100644 --- a/source/backend/entities/ef/PimsLeaseConsultationHist.cs +++ b/source/backend/entities/ef/PimsLeaseConsultationHist.cs @@ -45,6 +45,11 @@ public partial class PimsLeaseConsultationHist [StringLength(20)] public string ConsultationStatusTypeCode { get; set; } + [Required] + [Column("CONSULTATION_OUTCOME_TYPE_CODE")] + [StringLength(20)] + public string ConsultationOutcomeTypeCode { get; set; } + [Column("OTHER_DESCRIPTION")] [StringLength(2000)] public string OtherDescription { get; set; } diff --git a/source/backend/entities/ef/PimsPropertyBoundaryLiteVw.cs b/source/backend/entities/ef/PimsPropertyBoundaryLiteVw.cs new file mode 100644 index 0000000000..f19e8dc411 --- /dev/null +++ b/source/backend/entities/ef/PimsPropertyBoundaryLiteVw.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.EntityFrameworkCore; +using NetTopologySuite.Geometries; + +namespace Pims.Dal.Entities; + +[Keyless] +public partial class PimsPropertyBoundaryLiteVw +{ + [Column("PROPERTY_ID")] + public long PropertyId { get; set; } + + [Column("GEOMETRY", TypeName = "geometry")] + public Geometry Geometry { get; set; } + + [Column("IS_OWNED")] + public bool IsOwned { get; set; } + + [Column("IS_RETIRED")] + public bool? IsRetired { get; set; } + + [Column("IS_OTHER_INTEREST")] + public bool? IsOtherInterest { get; set; } + + [Column("IS_DISPOSED")] + public bool? IsDisposed { get; set; } + + [Column("HAS_ACTIVE_ACQUISITION_FILE")] + public bool? HasActiveAcquisitionFile { get; set; } + + [Column("HAS_ACTIVE_RESEARCH_FILE")] + public bool? HasActiveResearchFile { get; set; } +} diff --git a/source/backend/entities/ef/PimsPropertyLocationLiteVw.cs b/source/backend/entities/ef/PimsPropertyLocationLiteVw.cs new file mode 100644 index 0000000000..0e2186c007 --- /dev/null +++ b/source/backend/entities/ef/PimsPropertyLocationLiteVw.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.EntityFrameworkCore; +using NetTopologySuite.Geometries; + +namespace Pims.Dal.Entities; + +[Keyless] +public partial class PimsPropertyLocationLiteVw +{ + [Column("PROPERTY_ID")] + public long PropertyId { get; set; } + + [Column("GEOMETRY", TypeName = "geometry")] + public Geometry Geometry { get; set; } + + [Column("IS_OWNED")] + public bool IsOwned { get; set; } + + [Column("IS_RETIRED")] + public bool? IsRetired { get; set; } + + [Column("IS_OTHER_INTEREST")] + public bool? IsOtherInterest { get; set; } + + [Column("IS_DISPOSED")] + public bool? IsDisposed { get; set; } + + [Column("HAS_ACTIVE_ACQUISITION_FILE")] + public bool? HasActiveAcquisitionFile { get; set; } + + [Column("HAS_ACTIVE_RESEARCH_FILE")] + public bool? HasActiveResearchFile { get; set; } +} diff --git a/source/backend/entities/ef/PimsResearchFileNote.cs b/source/backend/entities/ef/PimsResearchFileNote.cs index 58947c7cb2..9bab82edfd 100644 --- a/source/backend/entities/ef/PimsResearchFileNote.cs +++ b/source/backend/entities/ef/PimsResearchFileNote.cs @@ -9,7 +9,6 @@ namespace Pims.Dal.Entities; /// /// Defines the relationship betwwen a research file and a note. /// -[PrimaryKey("ResearchFileNoteId", "ResearchFileId")] [Table("PIMS_RESEARCH_FILE_NOTE")] [Index("NoteId", Name = "RFLNOT_NOTE_ID_IDX")] [Index("ResearchFileId", Name = "RFLNOT_RESEARCH_FILE_ID_IDX")] @@ -20,7 +19,6 @@ public partial class PimsResearchFileNote [Column("RESEARCH_FILE_NOTE_ID")] public long ResearchFileNoteId { get; set; } - [Key] [Column("RESEARCH_FILE_ID")] public long ResearchFileId { get; set; } diff --git a/source/backend/tests/core/Entities/LeaseConsultationHelper.cs b/source/backend/tests/core/Entities/LeaseConsultationHelper.cs new file mode 100644 index 0000000000..c6550b4767 --- /dev/null +++ b/source/backend/tests/core/Entities/LeaseConsultationHelper.cs @@ -0,0 +1,55 @@ +using System; +using System.Linq; +using Pims.Dal; +using Pims.Dal.Entities; +using Entity = Pims.Dal.Entities; + +namespace Pims.Core.Test +{ + /// + /// EntityHelper static class, provides helper methods to create test entities. + /// + public static partial class EntityHelper + { + /// + /// Create a new instance of a lease consultation. + /// + /// + public static Entity.PimsLeaseConsultation CreateLeaseConsultationItem(long? leaseConsultationId = null, long? leaseId = null, long? personId = null, long? organizationId = null, PimsConsultationType type = null, PimsConsultationStatusType statusType = null, PimsConsultationOutcomeType outcomeType = null) + { + var consultationItem = new Entity.PimsLeaseConsultation() + { + Internal_Id = leaseConsultationId ?? 1, + LeaseId = leaseId ?? 1, + PersonId = personId, + OrganizationId = organizationId, + IsResponseReceived = false, + RequestedOn = DateTime.UtcNow, + ResponseReceivedDate = DateTime.UtcNow, + }; + consultationItem.ConsultationOutcomeTypeCodeNavigation = outcomeType ?? new Entity.PimsConsultationOutcomeType() { Id = "APPRDENIED", Description = "Denied", DbCreateUserid = "test", DbLastUpdateUserid = "test", DbLastUpdateTimestamp = System.DateTime.Now }; + consultationItem.ConsultationOutcomeTypeCode = consultationItem.ConsultationOutcomeTypeCodeNavigation.Id; + consultationItem.ConsultationStatusTypeCodeNavigation = statusType ?? new Entity.PimsConsultationStatusType() { Id = "REQCOMP", Description = "Required", DbCreateUserid = "test", DbLastUpdateUserid = "test", DbLastUpdateTimestamp = System.DateTime.Now }; + consultationItem.ConsultationStatusTypeCode = consultationItem.ConsultationStatusTypeCodeNavigation.Id; + consultationItem.ConsultationTypeCodeNavigation = type ?? new Entity.PimsConsultationType() { Id = "ACTIVE", Description = "Active", DbCreateUserid = "test", DbLastUpdateUserid = "test", DbLastUpdateTimestamp = System.DateTime.Now }; + consultationItem.ConsultationTypeCode = consultationItem.ConsultationTypeCodeNavigation.Id; + + return consultationItem; + } + + /// + /// Create a new instance of a lease consultation. + /// + /// + public static Entity.PimsLeaseConsultation CreateLeaseConsultationItem(this PimsContext context, long? leaseId = null, long? leaseConsultationId = null) + { + var statusType = context.PimsConsultationStatusTypes.FirstOrDefault() ?? throw new InvalidOperationException("Unable to find consultation status type."); + var itemType = context.PimsConsultationTypes.FirstOrDefault() ?? throw new InvalidOperationException("Unable to find consultation type."); + var outcomeType = context.PimsConsultationOutcomeTypes.FirstOrDefault() ?? throw new InvalidOperationException("Unable to find consultation outcome type."); + var consultation = EntityHelper.CreateLeaseConsultationItem(leaseId: leaseId, leaseConsultationId: leaseConsultationId, statusType: statusType, type: itemType, outcomeType: outcomeType); + context.PimsLeaseConsultations.Add(consultation); + + return consultation; + } + } +} diff --git a/source/backend/tests/unit/api/Controllers/Leases/ConsultationControllerTest.cs b/source/backend/tests/unit/api/Controllers/Leases/ConsultationControllerTest.cs new file mode 100644 index 0000000000..b4c772c6ff --- /dev/null +++ b/source/backend/tests/unit/api/Controllers/Leases/ConsultationControllerTest.cs @@ -0,0 +1,204 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using FluentAssertions; +using MapsterMapper; +using Microsoft.AspNetCore.Mvc; +using Moq; +using Pims.Api.Areas.Disposition.Controllers; +using Pims.Api.Areas.Lease.Controllers; +using Pims.Api.Helpers.Exceptions; +using Pims.Api.Models.Concepts.DispositionFile; +using Pims.Api.Models.Concepts.Lease; +using Pims.Api.Services; +using Pims.Core.Test; +using Pims.Dal.Entities; +using Pims.Dal.Exceptions; +using Pims.Dal.Security; +using Xunit; + +namespace Pims.Api.Test.Controllers +{ + [Trait("category", "unit")] + [Trait("category", "api")] + [Trait("group", "consultation")] + [ExcludeFromCodeCoverage] + public class ConsultationControllerTest + { + #region Variables + private Mock _service; + private ConsultationController _controller; + private IMapper _mapper; + #endregion + + public ConsultationControllerTest() + { + var helper = new TestHelper(); + this._controller = helper.CreateController(Permissions.LeaseEdit, Permissions.LeaseView); + this._mapper = helper.GetService(); + this._service = helper.GetService>(); + } + + #region Tests + [Fact] + public void GetByConsultationId() + { + // Arrange + var consultation = new PimsLeaseConsultation() { LeaseId = 1 }; + + this._service.Setup(m => m.GetConsultationById(It.IsAny())).Returns(consultation); + + // Act + var result = this._controller.GetLeaseConsultationById(1, 1); + + // Assert + this._service.Verify(m => m.GetConsultationById(It.IsAny()), Times.Once()); + } + + [Fact] + public void GetByConsultationId_BadLeaseId() + { + // Arrange + var consultation = new PimsLeaseConsultation(); + + this._service.Setup(m => m.GetConsultationById(It.IsAny())).Returns(consultation); + + // Act + Action act = () => this._controller.GetLeaseConsultationById(1, 1); + act.Should().Throw(); + } + + [Fact] + public void GetLeaseConsultations() + { + // Arrange + var consultation = new PimsLeaseConsultation() { LeaseId = 1 }; + + this._service.Setup(m => m.GetConsultations(It.IsAny())).Returns(new List() { consultation }); + + // Act + var result = this._controller.GetLeaseConsultations(1); + + // Assert + this._service.Verify(m => m.GetConsultations(It.IsAny()), Times.Once()); + } + + [Fact] + public void AddLeaseConsultation() + { + // Arrange + var consultation = new PimsLeaseConsultation() { LeaseId = 1 }; + + this._service.Setup(m => m.AddConsultation(It.IsAny())).Returns(consultation); + + // Act + var result = this._controller.AddLeaseConsultation(1, _mapper.Map(consultation)); + + // Assert + this._service.Verify(m => m.AddConsultation(It.IsAny()), Times.Once()); + } + + [Fact] + public void AddLeaseConsultation_BadLeaseId() + { + // Arrange + var consultation = new PimsLeaseConsultation() { LeaseId = 1 }; + + this._service.Setup(m => m.AddConsultation(It.IsAny())).Returns(consultation); + + // Act + Action act = () => this._controller.AddLeaseConsultation(2, _mapper.Map(consultation)); + act.Should().Throw(); + } + + [Fact] + public void UpdateLeaseConsultation() + { + // Arrange + var consultation = new PimsLeaseConsultation() { LeaseId = 1, LeaseConsultationId = 1 }; + + this._service.Setup(m => m.UpdateConsultation(It.IsAny())).Returns(consultation); + + // Act + var result = this._controller.UpdateLeaseConsultation(1, 1, _mapper.Map(consultation)); + + // Assert + this._service.Verify(m => m.UpdateConsultation(It.IsAny()), Times.Once()); + } + + [Fact] + public void UpdateLeaseConsultation_BadLeaseId() + { + // Arrange + var consultation = new PimsLeaseConsultation() { LeaseId = 1, LeaseConsultationId = 1 }; + + this._service.Setup(m => m.UpdateConsultation(It.IsAny())).Returns(consultation); + + // Act + Action act = () => this._controller.UpdateLeaseConsultation(2, 1, _mapper.Map(consultation)); + act.Should().Throw().WithMessage("Invalid LeaseId."); + } + + [Fact] + public void UpdateLeaseConsultation_BadConsultationId() + { + // Arrange + var consultation = new PimsLeaseConsultation() { LeaseId = 1, LeaseConsultationId = 1 }; + + this._service.Setup(m => m.UpdateConsultation(It.IsAny())).Returns(consultation); + + // Act + Action act = () => this._controller.UpdateLeaseConsultation(1, 2, _mapper.Map(consultation)); + act.Should().Throw().WithMessage("Invalid consultationId."); + } + + [Fact] + public void DeleteLeaseConsultation() + { + // Arrange + var consultation = new PimsLeaseConsultation() { LeaseId = 1, LeaseConsultationId = 1 }; + + this._service.Setup(m => m.GetConsultationById(It.IsAny())).Returns(consultation); + this._service.Setup(m => m.DeleteConsultation(It.IsAny())).Returns(true); + + // Act + var result = this._controller.DeleteLeaseConsultation(1, 1); + + // Assert + this._service.Verify(m => m.DeleteConsultation(It.IsAny()), Times.Once()); + result.Should().BeEquivalentTo(new JsonResult(true)); + } + + [Fact] + public void DeleteLeaseConsultation_False() + { + // Arrange + var consultation = new PimsLeaseConsultation() { LeaseId = 1, LeaseConsultationId = 1 }; + + this._service.Setup(m => m.GetConsultationById(It.IsAny())).Returns(consultation); + this._service.Setup(m => m.DeleteConsultation(It.IsAny())).Returns(false); + + // Act + var result = this._controller.DeleteLeaseConsultation(1, 1); + + // Assert + result.Should().BeEquivalentTo(new JsonResult(false)); + } + + [Fact] + public void DeleteLeaseConsultation_BadLeaseId() + { + // Arrange + var consultation = new PimsLeaseConsultation() { LeaseId = 2, LeaseConsultationId = 1 }; + + this._service.Setup(m => m.GetConsultationById(It.IsAny())).Returns(consultation); + this._service.Setup(m => m.DeleteConsultation(It.IsAny())).Returns(true); + + // Act + Action act = () => this._controller.DeleteLeaseConsultation(1, 1); + act.Should().Throw().WithMessage("Invalid lease id for the given consultation."); + } + + #endregion + } +} diff --git a/source/backend/tests/unit/api/Services/LeaseServiceTest.cs b/source/backend/tests/unit/api/Services/LeaseServiceTest.cs index 14f31b1e9d..b607655f6a 100644 --- a/source/backend/tests/unit/api/Services/LeaseServiceTest.cs +++ b/source/backend/tests/unit/api/Services/LeaseServiceTest.cs @@ -636,6 +636,176 @@ public void UpdateProperties_RemoveProperty_Success() #endregion + #region Consultations + [Fact] + public void GetConsultations_NoPermission() + { + // Arrange + var lease = EntityHelper.CreateLease(1); + + var service = this.CreateLeaseService(); + var consultationRepository = this._helper.GetService>(); + + consultationRepository.Setup(x => x.GetConsultationsByLease(It.IsAny())).Returns(new List()); + + // Act + Action act = () => service.GetConsultations(1); + + // Assert + act.Should().Throw(); + consultationRepository.Verify(x => x.GetConsultationsByLease(It.IsAny()), Times.Never); + } + + [Fact] + public void GetConsultations_Success() + { + // Arrange + var service = this.CreateLeaseService(Permissions.LeaseView); + var consultationRepository = this._helper.GetService>(); + + consultationRepository.Setup(x => x.GetConsultationsByLease(It.IsAny())).Returns(new List()); + + // Act + var result = service.GetConsultations(1); + + // Assert + consultationRepository.Verify(x => x.GetConsultationsByLease(It.IsAny()), Times.Once); + } + + [Fact] + public void GetConsultationById_NoPermission() + { + // Arrange + var service = this.CreateLeaseService(); + var consultationRepository = this._helper.GetService>(); + + consultationRepository.Setup(x => x.GetConsultationById(It.IsAny())).Returns(new PimsLeaseConsultation()); + + // Act + Action act = () => service.GetConsultationById(1); + + // Assert + act.Should().Throw(); + consultationRepository.Verify(x => x.GetConsultationById(It.IsAny()), Times.Never); + } + + [Fact] + public void GetConsultationById_Success() + { + // Arrange + var service = this.CreateLeaseService(Permissions.LeaseView); + var consultationRepository = this._helper.GetService>(); + + consultationRepository.Setup(x => x.GetConsultationById(It.IsAny())).Returns(new PimsLeaseConsultation()); + + // Act + var result = service.GetConsultationById(1); + + // Assert + consultationRepository.Verify(x => x.GetConsultationById(It.IsAny()), Times.Once); + } + + [Fact] + public void AddConsultation_NoPermission() + { + // Arrange + var service = this.CreateLeaseService(); + var consultationRepository = this._helper.GetService>(); + + consultationRepository.Setup(x => x.AddConsultation(It.IsAny())).Returns(new PimsLeaseConsultation()); + + // Act + Action act = () => service.AddConsultation(new PimsLeaseConsultation()); + + // Assert + act.Should().Throw(); + consultationRepository.Verify(x => x.AddConsultation(It.IsAny()), Times.Never); + } + + [Fact] + public void AddConsultation_Success() + { + // Arrange + var service = this.CreateLeaseService(Permissions.LeaseEdit); + var consultationRepository = this._helper.GetService>(); + + consultationRepository.Setup(x => x.AddConsultation(It.IsAny())).Returns(new PimsLeaseConsultation()); + + // Act + var result = service.AddConsultation(new PimsLeaseConsultation()); + + // Assert + consultationRepository.Verify(x => x.AddConsultation(It.IsAny()), Times.Once); + } + + [Fact] + public void Update_Consultation_NoPermission() + { + // Arrange + var service = this.CreateLeaseService(); + var consultationRepository = this._helper.GetService>(); + + consultationRepository.Setup(x => x.UpdateConsultation(It.IsAny())).Returns(new PimsLeaseConsultation()); + + // Act + Action act = () => service.UpdateConsultation(new PimsLeaseConsultation()); + + // Assert + act.Should().Throw(); + consultationRepository.Verify(x => x.UpdateConsultation(It.IsAny()), Times.Never); + } + + [Fact] + public void Update_Consultation_Success() + { + // Arrange + var service = this.CreateLeaseService(Permissions.LeaseEdit); + var consultationRepository = this._helper.GetService>(); + + consultationRepository.Setup(x => x.UpdateConsultation(It.IsAny())).Returns(new PimsLeaseConsultation()); + + // Act + var result = service.UpdateConsultation(new PimsLeaseConsultation()); + + // Assert + consultationRepository.Verify(x => x.UpdateConsultation(It.IsAny()), Times.Once); + } + + [Fact] + public void Delete_Consultation_NoPermission() + { + // Arrange + var service = this.CreateLeaseService(); + var consultationRepository = this._helper.GetService>(); + + consultationRepository.Setup(x => x.TryDeleteConsultation(It.IsAny())).Returns(true); + + // Act + Action act = () => service.DeleteConsultation(1); + + // Assert + act.Should().Throw(); + consultationRepository.Verify(x => x.TryDeleteConsultation(It.IsAny()), Times.Never); + } + + [Fact] + public void Delete_Consultation_Success() + { + // Arrange + var service = this.CreateLeaseService(Permissions.LeaseEdit); + var consultationRepository = this._helper.GetService>(); + + consultationRepository.Setup(x => x.TryDeleteConsultation(It.IsAny())).Returns(true); + + // Act + var result = service.DeleteConsultation(1); + + // Assert + consultationRepository.Verify(x => x.TryDeleteConsultation(It.IsAny()), Times.Once); + result.Should().BeTrue(); + } + #endregion + #endregion } } diff --git a/source/backend/tests/unit/dal/Repositories/ConsultationRepositoryTest.cs b/source/backend/tests/unit/dal/Repositories/ConsultationRepositoryTest.cs new file mode 100644 index 0000000000..83ed5c9bea --- /dev/null +++ b/source/backend/tests/unit/dal/Repositories/ConsultationRepositoryTest.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Pims.Core.Test; +using Pims.Dal.Entities; +using Pims.Dal.Repositories; +using Pims.Dal.Security; +using Xunit; + +namespace Pims.Dal.Test.Repositories +{ + public class ConsultationRepositoryTest + { + private readonly TestHelper _helper; + + public ConsultationRepositoryTest() + { + this._helper = new TestHelper(); + } + + private ConsultationRepository CreateWithPermissions(params Permissions[] permissions) + { + var user = PrincipalHelper.CreateForPermission(permissions); + this._helper.CreatePimsContext(user, true); + return this._helper.CreateRepository(user); + } + + [Fact] + public void GetConsultationByLease_Success() + { + // Arrange + var repository = CreateWithPermissions(Permissions.LeaseEdit); + + var consultation = EntityHelper.CreateLeaseConsultationItem(); + _helper.AddAndSaveChanges(consultation); + + // Act + var result = repository.GetConsultationsByLease(consultation.LeaseId); + + // Assert + result.Should().HaveCount(1); + } + + [Fact] + public void GetConsultationById_Success() + { + // Arrange + var repository = CreateWithPermissions(Permissions.LeaseEdit); + + var consultation = EntityHelper.CreateLeaseConsultationItem(); + _helper.AddAndSaveChanges(consultation); + + // Act + var result = repository.GetConsultationById(1); + + // Assert + result.LeaseConsultationId.Should().Be(1); + } + + [Fact] + public void GetConsultationById_KeyNotFoundException() + { + // Arrange + var repository = CreateWithPermissions(Permissions.LeaseEdit); + + // Act + Action act = () => repository.GetConsultationById(1); + + act.Should().Throw(); + + } + + [Fact] + public void AddConsultationDocument_Success() + { + // Arrange + var repository = CreateWithPermissions(Permissions.LeaseEdit); + + // Act + var result = repository.AddConsultation(EntityHelper.CreateLeaseConsultationItem()); + + // Assert + result.LeaseConsultationId.Should().Be(1); + } + + [Fact] + public void UpdateConsultation_Success() + { + // Arrange + var repository = CreateWithPermissions(Permissions.LeaseEdit); + var consultation = EntityHelper.CreateLeaseConsultationItem(); + + _helper.AddAndSaveChanges(consultation); + + var updatedConsultation = EntityHelper.CreateLeaseConsultationItem(); + updatedConsultation.IsResponseReceived = true; + + // Act + var result = repository.UpdateConsultation(updatedConsultation); + + // Assert + result.IsResponseReceived.Should().Be(true); + } + + [Fact] + public void UpdateConsultation_KeyNotFoundException() + { + // Arrange + var repository = CreateWithPermissions(Permissions.LeaseEdit); + + var updatedConsultation = EntityHelper.CreateLeaseConsultationItem(); + updatedConsultation.IsResponseReceived = true; + + // Act + Action act = () => repository.UpdateConsultation(updatedConsultation); + + // Assert + act.Should().Throw(); + } + + [Fact] + public void DeleteConsultationDocument_Success() + { + // Arrange + var repository = CreateWithPermissions(Permissions.LeaseEdit); + + var consultation = EntityHelper.CreateLeaseConsultationItem(); + _helper.AddAndSaveChanges(consultation); + + // Act + var result = repository.TryDeleteConsultation(consultation.LeaseConsultationId); + + // Assert + result.Should().BeTrue(); + } + + [Fact] + public void DeleteConsultationDocument_Null() + { + // Arrange + var repository = CreateWithPermissions(Permissions.LeaseEdit); + + // Act + var result = repository.TryDeleteConsultation(1); + + // Assert + result.Should().BeFalse(); + } + } +} diff --git a/source/frontend/src/constants/API.ts b/source/frontend/src/constants/API.ts index 4a9d786225..73c56d9d2d 100644 --- a/source/frontend/src/constants/API.ts +++ b/source/frontend/src/constants/API.ts @@ -59,6 +59,7 @@ export const CONTACT_METHOD_TYPES = 'PimsContactMethodType'; export const COUNTRY_TYPES = 'PimsCountry'; export const CONSULTATION_TYPES = 'PimsConsultationType'; export const CONSULTATION_STATUS_TYPES = 'PimsConsultationStatusType'; +export const CONSULTATION_OUTCOME_TYPES = 'PimsConsultationOutcomeType'; export const INSURANCE_TYPES = 'PimsInsuranceType'; export const LEASE_CATEGORY_TYPES = 'PimsLeaseCategoryType'; export const LEASE_INITIATOR_TYPES = 'PimsLeaseInitiatorType'; diff --git a/source/frontend/src/features/leases/detail/LeasePages/stakeholders/AddLeaseStakeholderForm.test.tsx b/source/frontend/src/features/leases/detail/LeasePages/stakeholders/AddLeaseStakeholderForm.test.tsx index ba76be505d..039d3e16c5 100644 --- a/source/frontend/src/features/leases/detail/LeasePages/stakeholders/AddLeaseStakeholderForm.test.tsx +++ b/source/frontend/src/features/leases/detail/LeasePages/stakeholders/AddLeaseStakeholderForm.test.tsx @@ -18,6 +18,8 @@ import { mockKeycloak, renderAsync, RenderOptions, userEvent } from '@/utils/tes import AddLeaseStakeholderForm, { IAddLeaseStakeholderFormProps } from './AddLeaseStakeholderForm'; import { FormStakeholder } from './models'; import { createRef } from 'react'; +import { ApiGen_Concepts_LeaseStakeholder } from '@/models/api/generated/ApiGen_Concepts_LeaseStakeholder'; +import { ApiGen_Concepts_LeaseStakeholderType } from '@/models/api/generated/ApiGen_Concepts_LeaseStakeholderType'; const history = createMemoryHistory(); const storeState = { @@ -36,69 +38,62 @@ vi.mock('@/hooks/pims-api/useApiContacts', () => ({ }, })); -export const leaseStakeholderTypesList = [ +export const leaseStakeholderTypesList: ApiGen_Concepts_LeaseStakeholderType[] = [ { + id: 1, code: 'ASGN', description: 'Assignee', isPayableRelated: false, - isDisplayed: true, isDisabled: false, displayOrder: null, - rowVersion: null, }, { + id: 2, code: 'OWNER', description: 'Owner', isPayableRelated: true, - isDisplayed: true, isDisabled: false, displayOrder: null, - rowVersion: null, }, { + id: 3, code: 'OWNREP', description: 'Owner Representative', isPayableRelated: true, - isDisplayed: true, isDisabled: false, displayOrder: null, - rowVersion: null, }, { + id: 4, code: 'PMGR', description: 'Property manager', isPayableRelated: false, - isDisplayed: true, isDisabled: false, displayOrder: null, - rowVersion: null, }, { + id: 5, code: 'REP', description: 'Representative', isPayableRelated: false, - isDisplayed: true, isDisabled: false, displayOrder: null, - rowVersion: null, }, { + id: 6, code: 'TEN', description: 'Tenant', isPayableRelated: false, - isDisplayed: true, isDisabled: false, displayOrder: null, - rowVersion: null, }, { + id: 7, code: 'UNK', description: 'Unknown', isPayableRelated: false, - isDisplayed: true, isDisabled: false, displayOrder: null, - rowVersion: null, }, ]; diff --git a/source/frontend/src/features/mapSideBar/lease/tabs/consultations/detail/ConsultationListContainer.test.tsx b/source/frontend/src/features/mapSideBar/lease/tabs/consultations/detail/ConsultationListContainer.test.tsx new file mode 100644 index 0000000000..f087be7057 --- /dev/null +++ b/source/frontend/src/features/mapSideBar/lease/tabs/consultations/detail/ConsultationListContainer.test.tsx @@ -0,0 +1,164 @@ +import { FormikProps } from 'formik'; +import { createMemoryHistory } from 'history'; +import { forwardRef } from 'react'; + +import { mockLookups } from '@/mocks/lookups.mock'; +import { lookupCodesSlice } from '@/store/slices/lookupCodes'; +import { act, render, RenderOptions, waitFor } from '@/utils/test-utils'; + +import { vi } from 'vitest'; +import { useConsultationProvider } from '@/hooks/repositories/useConsultationProvider'; +import { IConsultationListViewProps } from './ConsultationListView'; +import ConsultationListContainer, { IConsultationListProps } from './ConsultationListContainer'; +import { getMockApiConsultation } from '@/mocks/consultations.mock'; + +const history = createMemoryHistory(); +const storeState = { + [lookupCodesSlice.name]: { lookupCodes: mockLookups }, +}; + +const mockGetApi = { + error: undefined, + response: undefined, + execute: vi.fn(), + loading: false, + status: 200, +}; + +const mockDeleteApi = { + error: undefined, + response: undefined, + execute: vi.fn(), + loading: false, + status: 200, +}; + +vi.mock('@/hooks/repositories/useConsultationProvider'); +vi.mocked(useConsultationProvider).mockImplementation(() => ({ + getLeaseConsultations: mockGetApi, + deleteLeaseConsultation: mockDeleteApi, + addLeaseConsultation: {} as any, //unused + getLeaseConsultationById: {} as any, + updateLeaseConsultation: {} as any, +})); + +describe('ConsultationListContainer component', () => { + // render component under test + + let viewProps: IConsultationListViewProps; + const View = forwardRef, IConsultationListViewProps>((props, ref) => { + viewProps = props; + return <>; + }); + + const setup = (renderOptions: RenderOptions & { props?: Partial }) => { + const utils = render( + , + { + ...renderOptions, + store: storeState, + history, + }, + ); + + return { + ...utils, + }; + }; + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('renders as expected', () => { + mockGetApi.response = [ + { ...getMockApiConsultation(), id: 1 }, + { ...getMockApiConsultation(), id: 2 }, + ]; + const { asFragment } = setup({}); + expect(asFragment()).toMatchSnapshot(); + }); + + it('calls getLeaseConsultations', async () => { + setup({}); + + await waitFor(() => { + expect(mockGetApi.execute).toHaveBeenCalledTimes(1); + }); + }); + + it('returns the lease consultations returned from the api', async () => { + mockGetApi.execute.mockResolvedValue([ + { ...getMockApiConsultation(), id: 1 }, + { ...getMockApiConsultation(), id: 2 }, + ]); + setup({}); + + await waitFor(() => { + expect(viewProps.consultations).toHaveLength(2); + }); + }); + + it('handles onAdd request with expected navigation', async () => { + mockGetApi.response = []; + setup({}); + + await act(async () => { + viewProps.onAdd(); + }); + + await waitFor(() => { + expect(history.location.pathname).toBe('//consultations/add'); + }); + }); + + it('handles onEdit request with expected navigation', async () => { + mockGetApi.response = []; + setup({}); + + await act(async () => { + viewProps.onEdit(1); + }); + + await waitFor(() => { + expect(history.location.pathname).toBe('//consultations/1/edit'); + }); + }); + + it('handles onDelete by calling the expected API', async () => { + mockGetApi.response = []; + setup({}); + + await act(async () => { + viewProps.onDelete(1); + }); + + await waitFor(() => { + expect(mockDeleteApi.execute).toHaveBeenCalled(); + }); + }); + + it('after calling onDelete, refreshes list of consultations', async () => { + mockGetApi.response = []; + setup({}); + + await act(async () => { + viewProps.onDelete(1); + }); + + await waitFor(() => { + expect(mockDeleteApi.execute).toHaveBeenCalled(); + }); + }); + + it('throws an error for an invalid lease id', async () => { + mockGetApi.response = []; + vi.spyOn(console, 'error').mockImplementation(() => {}); + const act = () => setup({ props: { leaseId: 0 } }); + expect(act).toThrowError('Unable to determine id of current file.'); + }); +}); diff --git a/source/frontend/src/features/mapSideBar/lease/tabs/consultations/detail/ConsultationListContainer.tsx b/source/frontend/src/features/mapSideBar/lease/tabs/consultations/detail/ConsultationListContainer.tsx index 981fa20d1a..c36f541f77 100644 --- a/source/frontend/src/features/mapSideBar/lease/tabs/consultations/detail/ConsultationListContainer.tsx +++ b/source/frontend/src/features/mapSideBar/lease/tabs/consultations/detail/ConsultationListContainer.tsx @@ -8,12 +8,12 @@ import { isValidId } from '@/utils'; import { LeasePageNames } from '../../../LeaseContainer'; import { IConsultationListViewProps } from './ConsultationListView'; -interface IConsultationListProps { +export interface IConsultationListProps { leaseId: number; View: React.FunctionComponent>; } -const ConsultationListContainer: React.FunctionComponent< +export const ConsultationListContainer: React.FunctionComponent< React.PropsWithChildren > = ({ leaseId, View }) => { const [leaseConsultations, setLeaseConsultations] = useState( diff --git a/source/frontend/src/features/mapSideBar/lease/tabs/consultations/detail/ConsultationListView.test.tsx b/source/frontend/src/features/mapSideBar/lease/tabs/consultations/detail/ConsultationListView.test.tsx new file mode 100644 index 0000000000..7d6199de64 --- /dev/null +++ b/source/frontend/src/features/mapSideBar/lease/tabs/consultations/detail/ConsultationListView.test.tsx @@ -0,0 +1,171 @@ +import { FormikProps } from 'formik'; +import { createMemoryHistory } from 'history'; +import { forwardRef } from 'react'; + +import { mockLookups } from '@/mocks/lookups.mock'; +import { lookupCodesSlice } from '@/store/slices/lookupCodes'; +import { + act, + findByTitle, + getByTitle, + mockKeycloak, + render, + RenderOptions, + userEvent, + waitFor, +} from '@/utils/test-utils'; + +import { vi } from 'vitest'; +import ConsultationListView, { IConsultationListViewProps } from './ConsultationListView'; +import { getMockApiConsultation } from '@/mocks/consultations.mock'; +import Claims from '@/constants/claims'; +import { user } from '@/constants/toasts'; +import { useApiContacts } from '@/hooks/pims-api/useApiContacts'; +import { usePersonRepository } from '@/features/contacts/repositories/usePersonRepository'; +import { useOrganizationRepository } from '@/features/contacts/repositories/useOrganizationRepository'; + +const history = createMemoryHistory(); +const storeState = { + [lookupCodesSlice.name]: { lookupCodes: mockLookups }, +}; + +const onAdd = vi.fn(); +const onEdit = vi.fn(); +const onDelete = vi.fn(); + +const mockGetPersonApi = { + error: undefined, + response: undefined, + execute: vi.fn(), + loading: false, + status: 200, +}; + +const mockGetOrganizationApi = { + error: undefined, + response: undefined, + execute: vi.fn(), + loading: false, + status: 200, +}; + +vi.mock('@/features/contacts/repositories/usePersonRepository'); +vi.mocked(usePersonRepository).mockImplementation(() => ({ + getPersonDetail: mockGetPersonApi, +})); + +vi.mock('@/features/contacts/repositories/useOrganizationRepository'); +vi.mocked(useOrganizationRepository).mockImplementation(() => ({ + getOrganizationDetail: mockGetOrganizationApi, +})); + +describe('ConsultationListView component', () => { + // render component under test + + const setup = ( + renderOptions: RenderOptions & { props?: Partial }, + ) => { + const utils = render( + , + { + ...renderOptions, + claims: [Claims.LEASE_EDIT], + useMockAuthentication: true, + store: storeState, + history, + }, + ); + + return { + ...utils, + }; + }; + + it('renders as expected', () => { + const consultations = [ + { ...getMockApiConsultation(), id: 1 }, + { ...getMockApiConsultation(), id: 2 }, + ]; + const { asFragment } = setup({ props: { consultations } }); + expect(asFragment()).toMatchSnapshot(); + }); + + it('displays loading spinner', async () => { + const { getByTestId } = setup({ props: { loading: true } }); + + const loading = getByTestId('filter-backdrop-loading'); + expect(loading).toBeVisible(); + }); + + it('has all expected categories', async () => { + const consultations = [ + { ...getMockApiConsultation(), id: 1 }, + { ...getMockApiConsultation(), id: 2 }, + ]; + const { getByText } = setup({ props: { consultations } }); + + expect(getByText('District', { exact: false })).toBeVisible(); + expect(getByText('First Nation', { exact: false })).toBeVisible(); + expect(getByText('Headquarter (HQ)', { exact: false })).toBeVisible(); + expect(getByText('Regional planning', { exact: false })).toBeVisible(); + expect(getByText('Regional property services', { exact: false })).toBeVisible(); + expect(getByText('Strategic Real Estate (SRE)', { exact: false })).toBeVisible(); + expect(getByText('Other', { exact: false })).toBeVisible(); + }); + + it('calls onAdd when clicked', async () => { + const consultations = [ + { ...getMockApiConsultation(), id: 1 }, + { ...getMockApiConsultation(), id: 2 }, + ]; + const { getByText } = setup({ props: { consultations } }); + + act(() => { + getByText('Add Approval / Consultation', { exact: false }).click(); + }); + + expect(onAdd).toHaveBeenCalledTimes(1); + }); + + it('displays the outcome as a header when clicked', async () => { + const consultations = [{ ...getMockApiConsultation(), id: 1 }]; + const { getByText } = setup({ props: { consultations } }); + + expect(getByText('Approval Denied')).toBeVisible(); + }); + + it('calls onEdit when clicked', async () => { + const consultations = [{ ...getMockApiConsultation(), id: 1 }]; + const { findByTitle } = setup({ props: { consultations } }); + + const editButton = await findByTitle('Edit Consultation'); + + act(() => { + userEvent.click(editButton); + }); + + expect(onEdit).toHaveBeenCalledTimes(1); + }); + + it('calls onDelete when clicked', async () => { + const consultations = [{ ...getMockApiConsultation(), id: 1 }]; + const { findByTitle, getByText } = setup({ props: { consultations } }); + + const deleteButton = await findByTitle('Delete Consultation'); + + act(() => { + userEvent.click(deleteButton); + }); + + expect( + getByText('You have selected to delete this Consultation. Do you want to proceed?'), + ).toBeVisible(); + }); +}); diff --git a/source/frontend/src/features/mapSideBar/lease/tabs/consultations/detail/ConsultationListView.tsx b/source/frontend/src/features/mapSideBar/lease/tabs/consultations/detail/ConsultationListView.tsx index 341e27fdb7..b5e7a71d85 100644 --- a/source/frontend/src/features/mapSideBar/lease/tabs/consultations/detail/ConsultationListView.tsx +++ b/source/frontend/src/features/mapSideBar/lease/tabs/consultations/detail/ConsultationListView.tsx @@ -104,11 +104,7 @@ export const ConsultationListView: React.FunctionComponent - - {group.consultationTypeCode === 'OTHER' - ? `${consultation.otherDescription} Approval / Consultation` - : `${group.consultationTypeDescription} Approval / Consultation`} - + {consultation.consultationOutcomeTypeCode?.description} {keycloak.hasClaim(Claims.LEASE_EDIT) && ( <> diff --git a/source/frontend/src/features/mapSideBar/lease/tabs/consultations/detail/__snapshots__/ConsultationListContainer.test.tsx.snap b/source/frontend/src/features/mapSideBar/lease/tabs/consultations/detail/__snapshots__/ConsultationListContainer.test.tsx.snap new file mode 100644 index 0000000000..7d5d4a2c63 --- /dev/null +++ b/source/frontend/src/features/mapSideBar/lease/tabs/consultations/detail/__snapshots__/ConsultationListContainer.test.tsx.snap @@ -0,0 +1,10 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`ConsultationListContainer component > renders as expected 1`] = ` + +
+
+ +`; diff --git a/source/frontend/src/features/mapSideBar/lease/tabs/consultations/detail/__snapshots__/ConsultationListView.test.tsx.snap b/source/frontend/src/features/mapSideBar/lease/tabs/consultations/detail/__snapshots__/ConsultationListView.test.tsx.snap new file mode 100644 index 0000000000..ae8838d790 --- /dev/null +++ b/source/frontend/src/features/mapSideBar/lease/tabs/consultations/detail/__snapshots__/ConsultationListView.test.tsx.snap @@ -0,0 +1,1124 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`ConsultationListView component > renders as expected 1`] = ` + +
+
+ .c4.btn { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + padding: 0.4rem 1.2rem; + border: 0.2rem solid transparent; + border-radius: 0.4rem; + text-align: center; + -webkit-text-decoration: none; + text-decoration: none; + font-size: 1.8rem; + font-family: 'BCSans','Noto Sans',Verdana,Arial,sans-serif; + font-weight: 700; + -webkit-letter-spacing: 0.1rem; + -moz-letter-spacing: 0.1rem; + -ms-letter-spacing: 0.1rem; + letter-spacing: 0.1rem; + cursor: pointer; +} + +.c4.btn .Button__value { + width: -webkit-max-content; + width: -moz-max-content; + width: max-content; +} + +.c4.btn:hover { + -webkit-text-decoration: underline; + text-decoration: underline; + opacity: 0.8; +} + +.c4.btn:focus { + outline-width: 0.4rem; + outline-style: solid; + outline-offset: 1px; + box-shadow: none; +} + +.c4.btn.btn-secondary { + background: none; +} + +.c4.btn.btn-info { + border: none; + background: none; + padding-left: 0.6rem; + padding-right: 0.6rem; +} + +.c4.btn.btn-info:hover, +.c4.btn.btn-info:active, +.c4.btn.btn-info:focus { + background: none; +} + +.c4.btn.btn-light { + border: none; +} + +.c4.btn.btn-dark { + border: none; +} + +.c4.btn.btn-link { + font-size: 1.6rem; + font-weight: 400; + background: none; + border: none; + -webkit-text-decoration: none; + text-decoration: none; + min-height: 2.5rem; + line-height: 3rem; + -webkit-box-pack: left; + -webkit-justify-content: left; + -ms-flex-pack: left; + justify-content: left; + -webkit-letter-spacing: unset; + -moz-letter-spacing: unset; + -ms-letter-spacing: unset; + letter-spacing: unset; + text-align: left; + padding: 0; +} + +.c4.btn.btn-link:hover, +.c4.btn.btn-link:active, +.c4.btn.btn-link:focus { + -webkit-text-decoration: underline; + text-decoration: underline; + border: none; + background: none; + box-shadow: none; + outline: none; +} + +.c4.btn.btn-link:disabled, +.c4.btn.btn-link.disabled { + background: none; + pointer-events: none; +} + +.c4.btn:disabled, +.c4.btn:disabled:hover { + box-shadow: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + pointer-events: none; + cursor: not-allowed; + opacity: 0.65; +} + +.c4.Button .Button__icon { + margin-right: 1.6rem; +} + +.c4.Button--icon-only:focus { + outline: none; +} + +.c4.Button--icon-only .Button__icon { + margin-right: 0; +} + +.c10.c10.btn { + color: #aaaaaa; + -webkit-text-decoration: none; + text-decoration: none; + line-height: unset; +} + +.c10.c10.btn .text { + display: none; +} + +.c10.c10.btn:hover, +.c10.c10.btn:active, +.c10.c10.btn:focus { + color: #d8292f; + -webkit-text-decoration: none; + text-decoration: none; + opacity: unset; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; +} + +.c10.c10.btn:hover .text, +.c10.c10.btn:active .text, +.c10.c10.btn:focus .text { + display: inline; + line-height: 2rem; +} + +.c12.required::before { + content: '*'; + position: absolute; + top: 0.75rem; + left: 0rem; +} + +.c11 { + font-weight: bold; +} + +.c5.c5 { + display: inline-block; + margin-left: 1.5rem; + margin-bottom: 0.5rem; +} + +.c8 { + float: right; + cursor: pointer; +} + +.c0 { + padding-top: 1rem; +} + +.c2 { + font-weight: bold; + border-bottom: 0.2rem solid; + margin-bottom: 2rem; +} + +.c1 { + margin: 1.6rem; + padding: 1.6rem; + background-color: white; + text-align: left; + border-radius: 0.5rem; +} + +.c6 { + background-color: white; + text-align: left; + border-radius: 0.5rem; +} + +.c3 { + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + -webkit-align-items: end; + -webkit-box-align: end; + -ms-flex-align: end; + align-items: end; + min-height: 4.5rem; +} + +.c3 .btn { + margin: 0; +} + +.c9 { + border: solid 0.2rem; + margin-bottom: 1.5rem; + border-radius: 0.5rem; +} + +.c7 { + color: white; + font-size: 1.4rem; + border-radius: 50%; + opacity: 0.8; + width: 2.2rem; + height: 2.2rem; + padding: 1rem; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; +} + +
+
+

+
+
+
+
+ Approval / Consultations +
+
+ +
+
+
+
+

+
+
+

+
+
+
+
+ + First nation + +
+
+
+
+
+

+
+

+ There are no approvals / consultations. +

+
+
+
+

+
+
+
+
+ + Strategic Real Estate (SRE) + +
+
+
+
+
+

+
+

+ There are no approvals / consultations. +

+
+
+
+

+
+
+
+
+ + Regional planning + +
+
+
+
+
+

+
+

+ There are no approvals / consultations. +

+
+
+
+

+
+
+
+
+ + Regional property services + +
+
+
+
+
+

+
+

+ There are no approvals / consultations. +

+
+
+
+

+
+
+
+
+ + District + +
+
+
+ 2 +
+
+
+
+
+ + + expand-section + + + + +
+
+

+
+
+
+

+
+
+
+
+
+ Approval Denied +
+
+ +
+
+ +
+
+
+
+
+

+
+
+
+ +
+
+ Jan 1, 2024 +
+
+
+
+ +
+
+
+
+
+ +
+
+ Yes +
+
+
+
+ +
+
+ Dec 1, 2024 +
+
+
+
+ +
+
+ test comment +
+
+
+
+
+
+
+

+
+
+
+
+
+ Approval Denied +
+
+ +
+
+ +
+
+
+
+
+

+
+
+
+ +
+
+ Jan 1, 2024 +
+
+
+
+ +
+
+
+
+
+ +
+
+ Yes +
+
+
+
+ +
+
+ Dec 1, 2024 +
+
+
+
+ +
+
+ test comment +
+
+
+
+
+
+
+
+

+
+
+
+
+ + Headquarter (HQ) + +
+
+
+
+
+

+
+

+ There are no approvals / consultations. +

+
+
+
+

+
+
+
+
+ + Other + +
+
+
+
+
+

+
+

+ There are no approvals / consultations. +

+
+
+
+
+
+ +`; diff --git a/source/frontend/src/features/mapSideBar/lease/tabs/consultations/edit/ConsultationAddContainer.test.tsx b/source/frontend/src/features/mapSideBar/lease/tabs/consultations/edit/ConsultationAddContainer.test.tsx new file mode 100644 index 0000000000..b9390088b1 --- /dev/null +++ b/source/frontend/src/features/mapSideBar/lease/tabs/consultations/edit/ConsultationAddContainer.test.tsx @@ -0,0 +1,149 @@ +import { FormikHelpers, FormikProps } from 'formik'; +import { createMemoryHistory } from 'history'; +import { forwardRef } from 'react'; + +import { mockLookups } from '@/mocks/lookups.mock'; +import { lookupCodesSlice } from '@/store/slices/lookupCodes'; +import { act, createAxiosError, render, RenderOptions, waitFor } from '@/utils/test-utils'; + +import { vi } from 'vitest'; +import { useConsultationProvider } from '@/hooks/repositories/useConsultationProvider'; +import { getMockApiConsultation } from '@/mocks/consultations.mock'; +import { IConsultationEditFormProps } from './ConsultationEditForm'; +import ConsultationAddContainer, { IConsultationAddProps } from './ConsultationAddContainer'; +import { ConsultationFormModel } from './models'; + +const history = createMemoryHistory(); +const storeState = { + [lookupCodesSlice.name]: { lookupCodes: mockLookups }, +}; + +const mockAddApi = { + error: undefined, + response: undefined, + execute: vi.fn(), + loading: false, + status: 200, +}; + +vi.mock('@/hooks/repositories/useConsultationProvider'); +vi.mocked(useConsultationProvider).mockImplementation(() => ({ + addLeaseConsultation: mockAddApi, + deleteLeaseConsultation: {} as any, + getLeaseConsultations: {} as any, //unused + getLeaseConsultationById: {} as any, + updateLeaseConsultation: {} as any, +})); + +const onSuccess = vi.fn(); + +describe('ConsultationAddContainer component', () => { + // render component under test + + let viewProps: IConsultationEditFormProps; + const View = forwardRef, IConsultationEditFormProps>((props, ref) => { + viewProps = props; + return <>; + }); + + const setup = (renderOptions: RenderOptions & { props?: Partial }) => { + const utils = render( + , + { + ...renderOptions, + store: storeState, + history, + }, + ); + + return { + ...utils, + }; + }; + + beforeEach(() => { + history.push('/lease/1/consultations/add'); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('renders as expected', () => { + const { asFragment } = setup({}); + expect(asFragment()).toMatchSnapshot(); + }); + + it('calls expected route when cancelled', async () => { + setup({}); + + await act(async () => { + viewProps.onCancel(); + }); + + expect(history.location.pathname).toEqual('/lease/1'); + }); + + it('submits data when onSubmit called', async () => { + setup({}); + + const formikHelpers: Partial> = { + setSubmitting: vi.fn(), + resetForm: vi.fn(), + }; + await act(async () => { + viewProps.onSubmit( + ConsultationFormModel.fromApi(getMockApiConsultation(), null, null), + formikHelpers as FormikHelpers, + ); + }); + + await waitFor(() => { + expect(mockAddApi.execute).toHaveBeenCalledTimes(1); + }); + }); + + it('submits data when onSubmit called history is updated', async () => { + setup({}); + + const formikHelpers: Partial> = { + setSubmitting: vi.fn(), + resetForm: vi.fn(), + }; + await act(async () => { + viewProps.onSubmit( + ConsultationFormModel.fromApi(getMockApiConsultation(), null, null), + formikHelpers as FormikHelpers, + ); + }); + + await waitFor(() => { + expect(history.location.pathname).toEqual('/lease/1/consultations/add'); + }); + }); + + it('displays error when add fails', async () => { + mockAddApi.execute.mockRejectedValue(createAxiosError(500)); + const { getByText } = setup({}); + + const formikHelpers: Partial> = { + setSubmitting: vi.fn(), + resetForm: vi.fn(), + }; + await act(async () => { + viewProps.onSubmit( + ConsultationFormModel.fromApi(getMockApiConsultation(), null, null), + formikHelpers as FormikHelpers, + ); + }); + + await waitFor(() => { + expect(getByText('Unable to save. Please try again.')).toBeVisible(); + }); + }); +}); diff --git a/source/frontend/src/features/mapSideBar/lease/tabs/consultations/edit/ConsultationEditForm.test.tsx b/source/frontend/src/features/mapSideBar/lease/tabs/consultations/edit/ConsultationEditForm.test.tsx new file mode 100644 index 0000000000..7e01e8eb1d --- /dev/null +++ b/source/frontend/src/features/mapSideBar/lease/tabs/consultations/edit/ConsultationEditForm.test.tsx @@ -0,0 +1,198 @@ +import { FormikProps } from 'formik'; +import { createMemoryHistory } from 'history'; +import { forwardRef } from 'react'; + +import { mockLookups } from '@/mocks/lookups.mock'; +import { lookupCodesSlice } from '@/store/slices/lookupCodes'; +import { + act, + fillInput, + findByTitle, + getByTitle, + mockKeycloak, + render, + RenderOptions, + userEvent, + waitFor, +} from '@/utils/test-utils'; + +import { vi } from 'vitest'; +import { getMockApiConsultation } from '@/mocks/consultations.mock'; +import Claims from '@/constants/claims'; +import { user } from '@/constants/toasts'; +import { useApiContacts } from '@/hooks/pims-api/useApiContacts'; +import { usePersonRepository } from '@/features/contacts/repositories/usePersonRepository'; +import { useOrganizationRepository } from '@/features/contacts/repositories/useOrganizationRepository'; +import ConsultationListView, { IConsultationListViewProps } from '../detail/ConsultationListView'; +import ConsultationEditForm, { IConsultationEditFormProps } from './ConsultationEditForm'; +import { ConsultationFormModel } from './models'; +import { getEmptyPerson } from '@/mocks/contacts.mock'; +import { ApiGen_CodeTypes_ConsultationOutcomeTypes } from '@/models/api/generated/ApiGen_CodeTypes_ConsultationOutcomeTypes'; + +const history = createMemoryHistory(); +const storeState = { + [lookupCodesSlice.name]: { lookupCodes: mockLookups }, +}; + +const onSubmit = vi.fn(); +const onCancel = vi.fn(); + +const mockGetPersonApi = { + error: undefined, + response: undefined, + execute: vi.fn(), + loading: false, + status: 200, +}; + +const mockGetOrganizationApi = { + error: undefined, + response: undefined, + execute: vi.fn(), + loading: false, + status: 200, +}; + +vi.mock('@/features/contacts/repositories/usePersonRepository'); +vi.mocked(usePersonRepository).mockImplementation(() => ({ + getPersonDetail: mockGetPersonApi, +})); + +vi.mock('@/features/contacts/repositories/useOrganizationRepository'); +vi.mocked(useOrganizationRepository).mockImplementation(() => ({ + getOrganizationDetail: mockGetOrganizationApi, +})); + +describe('ConsultationEditForm component', () => { + // render component under test + + const setup = ( + renderOptions: RenderOptions & { props?: Partial }, + ) => { + const utils = render( + , + { + ...renderOptions, + claims: [Claims.LEASE_EDIT], + useMockAuthentication: true, + store: storeState, + history, + }, + ); + + return { + ...utils, + }; + }; + + it('renders as expected', () => { + const { asFragment } = setup({}); + expect(asFragment()).toMatchSnapshot(); + }); + + it('displays loading spinner', async () => { + const { getByTestId } = setup({ props: { isLoading: true } }); + + const loading = getByTestId('filter-backdrop-loading'); + expect(loading).toBeVisible(); + }); + + it('cancels if no changes have been made', async () => { + const { getByText } = setup({}); + + act(() => { + userEvent.click(getByText('Cancel')); + }); + + expect(onCancel).toHaveBeenCalledTimes(1); + }); + + it('does not cancel if changes have been made', async () => { + const { getByText, findAllByDisplayValue, container } = setup({}); + + await act(async () => { + await fillInput(container, 'comment', 'test comments', 'textarea'); + }); + + await findAllByDisplayValue('test comments'); + + act(() => { + userEvent.click(getByText('Cancel')); + }); + + expect(onCancel).not.toHaveBeenCalled(); + }); + + it('cannot save when form has not been edited', async () => { + const { getByText } = setup({}); + + expect(getByText('Save').parentElement).toBeDisabled(); + }); + + it('submit calls onSubmit when changes have been made', async () => { + const { getByText, container } = setup({ + props: { + initialValues: ConsultationFormModel.fromApi( + { + ...getMockApiConsultation(), + consultationOutcomeTypeCode: { + id: ApiGen_CodeTypes_ConsultationOutcomeTypes.APPRGRANTED, + description: 'Approved', + isDisabled: false, + displayOrder: 1, + }, + }, + getEmptyPerson(), + null, + ), + }, + }); + + await act(async () => { + await fillInput(container, 'comment', 'test comments', 'textarea'); + }); + + act(() => { + userEvent.click(getByText('Save')); + }); + + await waitFor(() => { + expect(onSubmit).toHaveBeenCalledTimes(1); + }); + }); + + it('Comments become required if certain outcomes are selected', async () => { + const { getByText, container } = setup({ + props: { + initialValues: ConsultationFormModel.fromApi( + { + ...getMockApiConsultation(), + consultationOutcomeTypeCode: { + id: ApiGen_CodeTypes_ConsultationOutcomeTypes.APPRDENIED, + description: 'Denied', + isDisabled: false, + displayOrder: 1, + }, + }, + getEmptyPerson(), + null, + ), + }, + }); + + await act(async () => { + await fillInput(container, 'comment', '', 'textarea'); + }); + + expect(getByText('Please add comments')).toBeVisible(); + }); +}); diff --git a/source/frontend/src/features/mapSideBar/lease/tabs/consultations/edit/ConsultationEditForm.tsx b/source/frontend/src/features/mapSideBar/lease/tabs/consultations/edit/ConsultationEditForm.tsx index 9cf796a5a5..61358466ff 100644 --- a/source/frontend/src/features/mapSideBar/lease/tabs/consultations/edit/ConsultationEditForm.tsx +++ b/source/frontend/src/features/mapSideBar/lease/tabs/consultations/edit/ConsultationEditForm.tsx @@ -17,6 +17,7 @@ import { StyledFormWrapper } from '@/features/mapSideBar/shared/styles'; import useLookupCodeHelpers from '@/hooks/useLookupCodeHelpers'; import { getCancelModalProps, useModalContext } from '@/hooks/useModalContext'; import { isOrganizationResult } from '@/interfaces'; +import { ApiGen_CodeTypes_ConsultationOutcomeTypes } from '@/models/api/generated/ApiGen_CodeTypes_ConsultationOutcomeTypes'; import { exists, isValidId } from '@/utils'; import { UpdateConsultationYupSchema } from './EditConsultationYupSchema'; @@ -42,6 +43,7 @@ export const ConsultationEditForm: React.FunctionComponent void, dirty: boolean) => { if (!dirty) { @@ -160,10 +162,34 @@ export const ConsultationEditForm: React.FunctionComponent )} + + } + > + + + + + + + + + + +
+
+
+
+
+ +
+
+
+
+
+ + + + +
+
+
+
+
+
+
+ +
+
+
+
+
+
+ Select from contacts + +
+
+ +
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ + + + +
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+`; diff --git a/source/frontend/src/features/mapSideBar/lease/tabs/consultations/edit/__snapshots__/ConsultationUpdateContainer.test.tsx.snap b/source/frontend/src/features/mapSideBar/lease/tabs/consultations/edit/__snapshots__/ConsultationUpdateContainer.test.tsx.snap new file mode 100644 index 0000000000..4cb4a366b4 --- /dev/null +++ b/source/frontend/src/features/mapSideBar/lease/tabs/consultations/edit/__snapshots__/ConsultationUpdateContainer.test.tsx.snap @@ -0,0 +1,10 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`ConsultationUpdateContainer component > renders as expected 1`] = ` + +
+
+ +`; diff --git a/source/frontend/src/features/mapSideBar/lease/tabs/consultations/edit/models.ts b/source/frontend/src/features/mapSideBar/lease/tabs/consultations/edit/models.ts index 0e7b893654..8c38b59d57 100644 --- a/source/frontend/src/features/mapSideBar/lease/tabs/consultations/edit/models.ts +++ b/source/frontend/src/features/mapSideBar/lease/tabs/consultations/edit/models.ts @@ -14,6 +14,8 @@ export class ConsultationFormModel { public consultationTypeCode: string; public consultationTypeDescription: string; public consultationStatusTypeDescription: string; + public consultationOutcomeTypeCode: string; + public consultationOutcomeTypeCodeDescription: string; public otherDescription: string; public requestedOn: string; public isResponseReceived: boolean; @@ -27,6 +29,8 @@ export class ConsultationFormModel { this.consultationTypeCode = ''; this.consultationTypeDescription = ''; this.consultationStatusTypeDescription = ''; + this.consultationOutcomeTypeCode = ''; + this.consultationOutcomeTypeCodeDescription = ''; this.otherDescription = ''; this.requestedOn = ''; this.isResponseReceived = false; @@ -43,9 +47,12 @@ export class ConsultationFormModel { const consultation = new ConsultationFormModel(apiModel.leaseId); consultation.id = apiModel.id; consultation.consultationTypeCode = apiModel.consultationTypeCode?.id ?? ''; + consultation.consultationOutcomeTypeCode = apiModel.consultationOutcomeTypeCode?.id ?? ''; consultation.consultationTypeDescription = apiModel.consultationTypeCode?.description ?? ''; consultation.consultationStatusTypeDescription = apiModel.consultationStatusTypeCode?.description ?? ''; + consultation.consultationOutcomeTypeCodeDescription = + apiModel.consultationOutcomeTypeCode?.description ?? ''; consultation.otherDescription = apiModel.otherDescription ?? ''; consultation.requestedOn = apiModel.requestedOn ?? ''; consultation.isResponseReceived = apiModel.isResponseReceived ?? false; @@ -78,6 +85,7 @@ export class ConsultationFormModel { primaryContactId: toNullableId(this.primaryContactId), consultationTypeCode: toTypeCodeNullable(this.consultationTypeCode), consultationStatusTypeCode: toTypeCodeNullable('UNKNOWN'), + consultationOutcomeTypeCode: toTypeCodeNullable(this.consultationOutcomeTypeCode), otherDescription: emptyStringtoNullable(this.otherDescription), requestedOn: emptyStringtoNullable(this.requestedOn), isResponseReceived: this.isResponseReceived, diff --git a/source/frontend/src/mocks/consultations.mock.ts b/source/frontend/src/mocks/consultations.mock.ts new file mode 100644 index 0000000000..e9462cd2c0 --- /dev/null +++ b/source/frontend/src/mocks/consultations.mock.ts @@ -0,0 +1,46 @@ +import { ApiGen_Concepts_ConsultationLease } from '@/models/api/generated/ApiGen_Concepts_ConsultationLease'; + +import { getEmptyPerson } from './contacts.mock'; +import { getMockApiLease } from './lease.mock'; + +export const getMockApiConsultation = (): ApiGen_Concepts_ConsultationLease => ({ + id: 4, + rowVersion: 2, + leaseId: 1, + personId: 1, + lease: getMockApiLease(), + person: getEmptyPerson(), + organizationId: null, + organization: null, + primaryContactId: null, + primaryContact: null, + consultationTypeCode: { + id: 'DISTRICT', + description: 'Active', + isDisabled: false, + displayOrder: 10, + }, + consultationStatusTypeCode: { + id: 'REQCOMP', + description: 'Required', + isDisabled: false, + displayOrder: 10, + }, + consultationOutcomeTypeCode: { + id: 'APPRDENIED', + description: 'Approval Denied', + isDisabled: false, + displayOrder: 10, + }, + otherDescription: null, + requestedOn: '2024-01-01', + isResponseReceived: true, + responseReceivedDate: '2024-12-01', + comment: 'test comment', + appCreateTimestamp: '2024-02-06T20:56:46.47', + appLastUpdateTimestamp: '2024-02-06T20:56:46.47', + appLastUpdateUserid: 'dbo', + appLastUpdateUserGuid: 'dbo', + appCreateUserid: 'dbo', + appCreateUserGuid: 'dbo', +}); diff --git a/source/frontend/src/mocks/lookups.mock.ts b/source/frontend/src/mocks/lookups.mock.ts index 644a21315a..f52ef23686 100644 --- a/source/frontend/src/mocks/lookups.mock.ts +++ b/source/frontend/src/mocks/lookups.mock.ts @@ -3558,6 +3558,41 @@ export const mockLookups: ILookupCode[] = [ displayOrder: 4, type: 'PimsConsultationStatusType', }, + { + id: 'APPRDENIED', + name: 'Approval denied', + isDisabled: false, + displayOrder: 1, + type: 'PimsConsultationOutcomeType', + }, + { + id: 'APPRGRANTED', + name: 'Approval granted', + isDisabled: false, + displayOrder: 2, + type: 'PimsConsultationOutcomeType', + }, + { + id: 'CONSCOMPLTD', + name: 'Consultation completed', + isDisabled: false, + displayOrder: 3, + type: 'PimsConsultationOutcomeType', + }, + { + id: 'CONSDISCONT', + name: 'Consultation discontinued', + isDisabled: false, + displayOrder: 4, + type: 'PimsConsultationOutcomeType', + }, + { + id: 'INPROGRESS', + name: 'In-progress', + isDisabled: false, + displayOrder: 4, + type: 'PimsConsultationOutcomeType', + }, { id: 'PARTIAL', name: 'Partial', diff --git a/source/frontend/src/models/api/generated/ApiGen_CodeTypes_ConsultationOutcomeTypes.ts b/source/frontend/src/models/api/generated/ApiGen_CodeTypes_ConsultationOutcomeTypes.ts new file mode 100644 index 0000000000..60e453ba9c --- /dev/null +++ b/source/frontend/src/models/api/generated/ApiGen_CodeTypes_ConsultationOutcomeTypes.ts @@ -0,0 +1,11 @@ +/** + * File autogenerated by TsGenerator. + * Do not manually modify, changes made to this file will be lost when this file is regenerated. + */ +export enum ApiGen_CodeTypes_ConsultationOutcomeTypes { + APPRDENIED = 'APPRDENIED', + APPRGRANTED = 'APPRGRANTED', + CONSCOMPLTD = 'CONSCOMPLTD', + CONSDISCONT = 'CONSDISCONT', + INPROGRESS = 'INPROGRESS', +} diff --git a/source/frontend/src/models/api/generated/ApiGen_Concepts_ConsultationLease.ts b/source/frontend/src/models/api/generated/ApiGen_Concepts_ConsultationLease.ts index bb55c26463..ea7941f93c 100644 --- a/source/frontend/src/models/api/generated/ApiGen_Concepts_ConsultationLease.ts +++ b/source/frontend/src/models/api/generated/ApiGen_Concepts_ConsultationLease.ts @@ -23,6 +23,7 @@ export interface ApiGen_Concepts_ConsultationLease extends ApiGen_Base_BaseAudit primaryContact: ApiGen_Concepts_Person | null; consultationTypeCode: ApiGen_Base_CodeType | null; consultationStatusTypeCode: ApiGen_Base_CodeType | null; + consultationOutcomeTypeCode: ApiGen_Base_CodeType | null; otherDescription: string | null; requestedOn: UtcIsoDate | null; isResponseReceived: boolean | null; diff --git a/source/frontend/src/models/api/generated/ApiGen_Concepts_LeaseStakeholderType.ts b/source/frontend/src/models/api/generated/ApiGen_Concepts_LeaseStakeholderType.ts index 1ae995726f..183ecf40ff 100644 --- a/source/frontend/src/models/api/generated/ApiGen_Concepts_LeaseStakeholderType.ts +++ b/source/frontend/src/models/api/generated/ApiGen_Concepts_LeaseStakeholderType.ts @@ -2,13 +2,10 @@ * File autogenerated by TsGenerator. * Do not manually modify, changes made to this file will be lost when this file is regenerated. */ -import { ApiGen_Base_BaseConcurrent } from './ApiGen_Base_BaseConcurrent'; +import { ApiGen_Concepts_CodeType } from './ApiGen_Concepts_CodeType'; // LINK: @backend/apimodels/Models/Concepts/Lease/LeaseStakeholderTypeModel.cs -export interface ApiGen_Concepts_LeaseStakeholderType extends ApiGen_Base_BaseConcurrent { - code: string | null; - description: string | null; +export interface ApiGen_Concepts_LeaseStakeholderType extends ApiGen_Concepts_CodeType { isPayableRelated: boolean; isDisabled: boolean; - displayOrder: number | null; } From aa53cb8de6dcd49eed9ea063daf098d79102ee90 Mon Sep 17 00:00:00 2001 From: devinleighsmith Date: Tue, 17 Sep 2024 14:13:30 -0700 Subject: [PATCH 2/4] psp-9149 add icon based on consultation outcome(s) of category. --- .../detail/ConsultationListView.test.tsx | 97 ++++++++++++++ .../detail/ConsultationListView.tsx | 76 ++++++++++- .../ConsultationListView.test.tsx.snap | 125 ++++++++++-------- .../ConsultationEditForm.test.tsx.snap | 2 +- 4 files changed, 241 insertions(+), 59 deletions(-) diff --git a/source/frontend/src/features/mapSideBar/lease/tabs/consultations/detail/ConsultationListView.test.tsx b/source/frontend/src/features/mapSideBar/lease/tabs/consultations/detail/ConsultationListView.test.tsx index 7d6199de64..76209704ea 100644 --- a/source/frontend/src/features/mapSideBar/lease/tabs/consultations/detail/ConsultationListView.test.tsx +++ b/source/frontend/src/features/mapSideBar/lease/tabs/consultations/detail/ConsultationListView.test.tsx @@ -7,6 +7,7 @@ import { lookupCodesSlice } from '@/store/slices/lookupCodes'; import { act, findByTitle, + getByTestId, getByTitle, mockKeycloak, render, @@ -23,6 +24,8 @@ import { user } from '@/constants/toasts'; import { useApiContacts } from '@/hooks/pims-api/useApiContacts'; import { usePersonRepository } from '@/features/contacts/repositories/usePersonRepository'; import { useOrganizationRepository } from '@/features/contacts/repositories/useOrganizationRepository'; +import { ApiGen_CodeTypes_ConsultationOutcomeTypes } from '@/models/api/generated/ApiGen_CodeTypes_ConsultationOutcomeTypes'; +import { toTypeCodeNullable } from '@/utils/formUtils'; const history = createMemoryHistory(); const storeState = { @@ -168,4 +171,98 @@ describe('ConsultationListView component', () => { getByText('You have selected to delete this Consultation. Do you want to proceed?'), ).toBeVisible(); }); + + it('displays error consultation icon when there is at lease one consultation in error status', async () => { + const consultations = [ + { + ...getMockApiConsultation(), + id: 1, + consultationOutcomeTypeCode: toTypeCodeNullable( + ApiGen_CodeTypes_ConsultationOutcomeTypes.APPRDENIED, + ), + }, + { + ...getMockApiConsultation(), + id: 2, + consultationOutcomeTypeCode: toTypeCodeNullable( + ApiGen_CodeTypes_ConsultationOutcomeTypes.CONSDISCONT, + ), + }, + { + ...getMockApiConsultation(), + id: 3, + consultationOutcomeTypeCode: toTypeCodeNullable( + ApiGen_CodeTypes_ConsultationOutcomeTypes.APPRGRANTED, + ), + }, + { + ...getMockApiConsultation(), + id: 4, + consultationOutcomeTypeCode: toTypeCodeNullable( + ApiGen_CodeTypes_ConsultationOutcomeTypes.CONSCOMPLTD, + ), + }, + { + ...getMockApiConsultation(), + id: 5, + consultationOutcomeTypeCode: toTypeCodeNullable( + ApiGen_CodeTypes_ConsultationOutcomeTypes.INPROGRESS, + ), + }, + ]; + const { getByTestId } = setup({ props: { consultations } }); + + expect(getByTestId('error-icon')).toBeVisible(); + }); + + it('displays info consultation icon when there is at least one in progress outcome and no error outcomes', async () => { + const consultations = [ + { + ...getMockApiConsultation(), + id: 3, + consultationOutcomeTypeCode: toTypeCodeNullable( + ApiGen_CodeTypes_ConsultationOutcomeTypes.APPRGRANTED, + ), + }, + { + ...getMockApiConsultation(), + id: 4, + consultationOutcomeTypeCode: toTypeCodeNullable( + ApiGen_CodeTypes_ConsultationOutcomeTypes.CONSCOMPLTD, + ), + }, + { + ...getMockApiConsultation(), + id: 5, + consultationOutcomeTypeCode: toTypeCodeNullable( + ApiGen_CodeTypes_ConsultationOutcomeTypes.INPROGRESS, + ), + }, + ]; + const { getByTestId } = setup({ props: { consultations } }); + + expect(getByTestId('info-icon')).toBeVisible(); + }); + + it('displays ok consultation icon when all outcomes are in a completed status', async () => { + const consultations = [ + { + ...getMockApiConsultation(), + id: 3, + consultationOutcomeTypeCode: toTypeCodeNullable( + ApiGen_CodeTypes_ConsultationOutcomeTypes.APPRGRANTED, + ), + }, + { + ...getMockApiConsultation(), + id: 4, + consultationOutcomeTypeCode: toTypeCodeNullable( + ApiGen_CodeTypes_ConsultationOutcomeTypes.CONSCOMPLTD, + ), + }, + ]; + const { getByTestId } = setup({ props: { consultations } }); + + expect(getByTestId('ok-icon')).toBeVisible(); + }); }); diff --git a/source/frontend/src/features/mapSideBar/lease/tabs/consultations/detail/ConsultationListView.tsx b/source/frontend/src/features/mapSideBar/lease/tabs/consultations/detail/ConsultationListView.tsx index b5e7a71d85..b7212c40db 100644 --- a/source/frontend/src/features/mapSideBar/lease/tabs/consultations/detail/ConsultationListView.tsx +++ b/source/frontend/src/features/mapSideBar/lease/tabs/consultations/detail/ConsultationListView.tsx @@ -1,6 +1,6 @@ import { useMemo } from 'react'; import { Col, Row } from 'react-bootstrap'; -import { FaPlus, FaTrash } from 'react-icons/fa'; +import { FaCheckCircle, FaExclamationCircle, FaPlus, FaTimesCircle, FaTrash } from 'react-icons/fa'; import styled from 'styled-components'; import { StyledRemoveLinkButton } from '@/components/common/buttons/RemoveButton'; @@ -12,11 +12,13 @@ import { SectionField } from '@/components/common/Section/SectionField'; import { StyledSummarySection } from '@/components/common/Section/SectionStyles'; import { SectionListHeader } from '@/components/common/SectionListHeader'; import TooltipIcon from '@/components/common/TooltipIcon'; +import TooltipWrapper from '@/components/common/TooltipWrapper'; import * as API from '@/constants/API'; import Claims from '@/constants/claims'; import useKeycloakWrapper from '@/hooks/useKeycloakWrapper'; import useLookupCodeHelpers from '@/hooks/useLookupCodeHelpers'; import { getDeleteModalProps, useModalContext } from '@/hooks/useModalContext'; +import { ApiGen_CodeTypes_ConsultationOutcomeTypes } from '@/models/api/generated/ApiGen_CodeTypes_ConsultationOutcomeTypes'; import { ApiGen_Concepts_ConsultationLease } from '@/models/api/generated/ApiGen_Concepts_ConsultationLease'; import { prettyFormatDate } from '@/utils'; import { booleanToYesNoString } from '@/utils/formUtils'; @@ -83,16 +85,17 @@ export const ConsultationListView: React.FunctionComponent + - {group.consultationTypeDescription} + {group.consultationTypeDescription} + {getOutcomeIcon(group.consultations)} {group.consultations.length > 0 && ( {group.consultations.length} )} - + } noPadding isCollapsable={!group.hasItems} @@ -105,7 +108,6 @@ export const ConsultationListView: React.FunctionComponent {consultation.consultationOutcomeTypeCode?.description} - {keycloak.hasClaim(Claims.LEASE_EDIT) && ( <> @@ -235,3 +237,67 @@ const StyledIconWrapper = styled.div` justify-content: center; align-items: center; `; + +const StyledConsultationHeader = styled(Row)` + svg.info { + color: ${props => props.theme.bcTokens.surfaceColorBackgroundDarkBlue}; + } + svg.error { + color: ${props => props.theme.bcTokens.typographyColorDanger}; + } + svg.ok { + color: ${props => props.theme.css.completedColor}; + } + svg { + vertical-align: baseline; + } +`; + +const getOutcomeIcon = (consultations: ApiGen_Concepts_ConsultationLease[]) => { + if (consultations.length === 0) { + return null; + } + + if ( + consultations.find(c => + [ + ApiGen_CodeTypes_ConsultationOutcomeTypes.APPRDENIED.toString(), + ApiGen_CodeTypes_ConsultationOutcomeTypes.CONSDISCONT.toString(), + ].includes(c?.consultationOutcomeTypeCode?.id), + ) + ) { + return ( + + + + ); + } else if ( + consultations.every(c => + [ + ApiGen_CodeTypes_ConsultationOutcomeTypes.APPRGRANTED.toString(), + ApiGen_CodeTypes_ConsultationOutcomeTypes.CONSCOMPLTD.toString(), + ].includes(c?.consultationOutcomeTypeCode?.id), + ) + ) { + return ( + + + + ); + } else { + return ( + + + + ); + } +}; diff --git a/source/frontend/src/features/mapSideBar/lease/tabs/consultations/detail/__snapshots__/ConsultationListView.test.tsx.snap b/source/frontend/src/features/mapSideBar/lease/tabs/consultations/detail/__snapshots__/ConsultationListView.test.tsx.snap index ae8838d790..e32ddbc68d 100644 --- a/source/frontend/src/features/mapSideBar/lease/tabs/consultations/detail/__snapshots__/ConsultationListView.test.tsx.snap +++ b/source/frontend/src/features/mapSideBar/lease/tabs/consultations/detail/__snapshots__/ConsultationListView.test.tsx.snap @@ -141,20 +141,20 @@ exports[`ConsultationListView component > renders as expected 1`] = ` margin-right: 0; } -.c10.c10.btn { +.c11.c11.btn { color: #aaaaaa; -webkit-text-decoration: none; text-decoration: none; line-height: unset; } -.c10.c10.btn .text { +.c11.c11.btn .text { display: none; } -.c10.c10.btn:hover, -.c10.c10.btn:active, -.c10.c10.btn:focus { +.c11.c11.btn:hover, +.c11.c11.btn:active, +.c11.c11.btn:focus { color: #d8292f; -webkit-text-decoration: none; text-decoration: none; @@ -168,21 +168,21 @@ exports[`ConsultationListView component > renders as expected 1`] = ` flex-direction: row; } -.c10.c10.btn:hover .text, -.c10.c10.btn:active .text, -.c10.c10.btn:focus .text { +.c11.c11.btn:hover .text, +.c11.c11.btn:active .text, +.c11.c11.btn:focus .text { display: inline; line-height: 2rem; } -.c12.required::before { +.c13.required::before { content: '*'; position: absolute; top: 0.75rem; left: 0rem; } -.c11 { +.c12 { font-weight: bold; } @@ -192,7 +192,7 @@ exports[`ConsultationListView component > renders as expected 1`] = ` margin-bottom: 0.5rem; } -.c8 { +.c9 { float: right; cursor: pointer; } @@ -237,13 +237,13 @@ exports[`ConsultationListView component > renders as expected 1`] = ` margin: 0; } -.c9 { +.c10 { border: solid 0.2rem; margin-bottom: 1.5rem; border-radius: 0.5rem; } -.c7 { +.c8 { color: white; font-size: 1.4rem; border-radius: 50%; @@ -265,6 +265,10 @@ exports[`ConsultationListView component > renders as expected 1`] = ` align-items: center; } +.c7 svg { + vertical-align: baseline; +} +
@@ -335,13 +339,13 @@ exports[`ConsultationListView component > renders as expected 1`] = ` class="col" >
First nation @@ -376,13 +380,13 @@ exports[`ConsultationListView component > renders as expected 1`] = ` class="col" >
Strategic Real Estate (SRE) @@ -417,13 +421,13 @@ exports[`ConsultationListView component > renders as expected 1`] = ` class="col" >
Regional planning @@ -458,13 +462,13 @@ exports[`ConsultationListView component > renders as expected 1`] = ` class="col" >
Regional property services @@ -499,22 +503,37 @@ exports[`ConsultationListView component > renders as expected 1`] = ` class="col" >
District + + +
2
@@ -525,7 +544,7 @@ exports[`ConsultationListView component > renders as expected 1`] = ` class="pl-8 d-flex align-items-end col-auto" > renders as expected 1`] = ` class="collapse" >
renders as expected 1`] = ` class="px-1 col-auto" >
Jan 1, 2024
@@ -682,7 +701,7 @@ exports[`ConsultationListView component > renders as expected 1`] = ` class="pr-0 text-left col-4" >
renders as expected 1`] = ` class="pr-0 text-left col-4" >
Yes
@@ -738,13 +757,13 @@ exports[`ConsultationListView component > renders as expected 1`] = ` class="pr-0 text-left col-4" >
Dec 1, 2024
@@ -756,7 +775,7 @@ exports[`ConsultationListView component > renders as expected 1`] = ` class="pr-0 text-left col-4" >
test comment
@@ -793,7 +812,7 @@ exports[`ConsultationListView component > renders as expected 1`] = `
renders as expected 1`] = ` class="px-1 col-auto" >
Jan 1, 2024
@@ -923,7 +942,7 @@ exports[`ConsultationListView component > renders as expected 1`] = ` class="pr-0 text-left col-4" >
renders as expected 1`] = ` class="pr-0 text-left col-4" >
Yes
@@ -979,13 +998,13 @@ exports[`ConsultationListView component > renders as expected 1`] = ` class="pr-0 text-left col-4" >
Dec 1, 2024
@@ -997,7 +1016,7 @@ exports[`ConsultationListView component > renders as expected 1`] = ` class="pr-0 text-left col-4" >
test comment
@@ -1048,13 +1067,13 @@ exports[`ConsultationListView component > renders as expected 1`] = ` class="col" >
Headquarter (HQ) @@ -1089,13 +1108,13 @@ exports[`ConsultationListView component > renders as expected 1`] = ` class="col" >
Other diff --git a/source/frontend/src/features/mapSideBar/lease/tabs/consultations/edit/__snapshots__/ConsultationEditForm.test.tsx.snap b/source/frontend/src/features/mapSideBar/lease/tabs/consultations/edit/__snapshots__/ConsultationEditForm.test.tsx.snap index efe7fc6286..cfbcf51895 100644 --- a/source/frontend/src/features/mapSideBar/lease/tabs/consultations/edit/__snapshots__/ConsultationEditForm.test.tsx.snap +++ b/source/frontend/src/features/mapSideBar/lease/tabs/consultations/edit/__snapshots__/ConsultationEditForm.test.tsx.snap @@ -1,6 +1,6 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`ConsultationListView component > renders as expected 1`] = ` +exports[`ConsultationEditForm component > renders as expected 1`] = `
Date: Thu, 26 Sep 2024 12:47:05 -0700 Subject: [PATCH 3/4] snapshot update. --- .../ConsultationListView.test.tsx.snap | 215 +++++++++++++----- 1 file changed, 156 insertions(+), 59 deletions(-) diff --git a/source/frontend/src/features/mapSideBar/lease/tabs/consultations/detail/__snapshots__/ConsultationListView.test.tsx.snap b/source/frontend/src/features/mapSideBar/lease/tabs/consultations/detail/__snapshots__/ConsultationListView.test.tsx.snap index e32ddbc68d..63b2a0bf9e 100644 --- a/source/frontend/src/features/mapSideBar/lease/tabs/consultations/detail/__snapshots__/ConsultationListView.test.tsx.snap +++ b/source/frontend/src/features/mapSideBar/lease/tabs/consultations/detail/__snapshots__/ConsultationListView.test.tsx.snap @@ -50,15 +50,37 @@ exports[`ConsultationListView component > renders as expected 1`] = ` .c4.btn:focus { outline-width: 0.4rem; outline-style: solid; + outline-color: var(--surface-color-primary-button-hover); outline-offset: 1px; box-shadow: none; } +.c4.btn.btn-primary { + color: #FFFFFF; + background-color: #013366; +} + +.c4.btn.btn-primary:hover, +.c4.btn.btn-primary:active, +.c4.btn.btn-primary:focus { + background-color: #1E5189; +} + .c4.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 { + color: #FFFFFF; + background-color: #013366; } .c4.btn.btn-info { + color: #9F9D9C; border: none; background: none; padding-left: 0.6rem; @@ -68,20 +90,66 @@ exports[`ConsultationListView component > renders as expected 1`] = ` .c4.btn.btn-info:hover, .c4.btn.btn-info:active, .c4.btn.btn-info:focus { + color: var(--surface-color-primary-button-hover); background: none; } .c4.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 { + color: #FFFFFF; + background-color: #606060; +} + .c4.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 { + color: #FFFFFF; + background-color: #474543; +} + +.c4.btn.btn-danger { + color: #FFFFFF; + background-color: #CE3E39; +} + +.c4.btn.btn-danger:hover, +.c4.btn.btn-danger:active, +.c4.btn.btn-danger:focus { + color: #FFFFFF; + background-color: #CE3E39; +} + +.c4.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 { + color: #FFFFFF; + border-color: #FCBA19; + background-color: #FCBA19; +} + .c4.btn.btn-link { font-size: 1.6rem; font-weight: 400; + color: var(--surface-color-primary-button-default); background: none; border: none; -webkit-text-decoration: none; @@ -103,6 +171,7 @@ exports[`ConsultationListView component > renders as expected 1`] = ` .c4.btn.btn-link:hover, .c4.btn.btn-link:active, .c4.btn.btn-link:focus { + color: var(--surface-color-primary-button-hover); -webkit-text-decoration: underline; text-decoration: underline; border: none; @@ -113,6 +182,7 @@ exports[`ConsultationListView component > renders as expected 1`] = ` .c4.btn.btn-link:disabled, .c4.btn.btn-link.disabled { + color: #9F9D9C; background: none; pointer-events: none; } @@ -141,20 +211,20 @@ exports[`ConsultationListView component > renders as expected 1`] = ` margin-right: 0; } -.c11.c11.btn { +.c12.c12.btn { color: #aaaaaa; -webkit-text-decoration: none; text-decoration: none; line-height: unset; } -.c11.c11.btn .text { +.c12.c12.btn .text { display: none; } -.c11.c11.btn:hover, -.c11.c11.btn:active, -.c11.c11.btn:focus { +.c12.c12.btn:hover, +.c12.c12.btn:active, +.c12.c12.btn:focus { color: #d8292f; -webkit-text-decoration: none; text-decoration: none; @@ -168,42 +238,56 @@ exports[`ConsultationListView component > renders as expected 1`] = ` flex-direction: row; } -.c11.c11.btn:hover .text, -.c11.c11.btn:active .text, -.c11.c11.btn:focus .text { +.c12.c12.btn:hover .text, +.c12.c12.btn:active .text, +.c12.c12.btn:focus .text { display: inline; line-height: 2rem; } -.c13.required::before { +.c14.required::before { content: '*'; position: absolute; top: 0.75rem; left: 0rem; } -.c12 { +.c13 { font-weight: bold; } -.c5.c5 { +.c5.btn.btn-primary, +.c5.btn.btn-primary:active { + background-color: #42814A; +} + +.c5.btn.btn-primary:hover, +.c5.btn.btn-primary:focus { + background-color: #2e8540; + outline-color: #2e8540; +} + +.c6.c6 { display: inline-block; margin-left: 1.5rem; margin-bottom: 0.5rem; } -.c9 { +.c10 { float: right; cursor: pointer; + font-size: 3.2rem; } .c0 { + background-color: #f2f2f2; padding-top: 1rem; } .c2 { font-weight: bold; - border-bottom: 0.2rem solid; + color: var(--theme-blue-100); + border-bottom: 0.2rem var(--theme-blue-90) solid; margin-bottom: 2rem; } @@ -215,7 +299,7 @@ exports[`ConsultationListView component > renders as expected 1`] = ` border-radius: 0.5rem; } -.c6 { +.c7 { background-color: white; text-align: left; border-radius: 0.5rem; @@ -237,13 +321,14 @@ exports[`ConsultationListView component > renders as expected 1`] = ` margin: 0; } -.c10 { - border: solid 0.2rem; +.c11 { + border: solid 0.2rem var(--theme-blue-90); margin-bottom: 1.5rem; border-radius: 0.5rem; } -.c8 { +.c9 { + background-color: #428bca; color: white; font-size: 1.4rem; border-radius: 50%; @@ -265,7 +350,19 @@ exports[`ConsultationListView component > renders as expected 1`] = ` align-items: center; } -.c7 svg { +.c8 svg.info { + color: #053662; +} + +.c8 svg.error { + color: #CE3E39; +} + +.c8 svg.ok { + color: #2e8540; +} + +.c8 svg { vertical-align: baseline; } @@ -296,7 +393,7 @@ exports[`ConsultationListView component > renders as expected 1`] = ` class="my-1 col-auto" >
Jan 1, 2024
@@ -701,7 +798,7 @@ exports[`ConsultationListView component > renders as expected 1`] = ` class="pr-0 text-left col-4" >
renders as expected 1`] = ` class="pr-0 text-left col-4" >
Yes
@@ -757,13 +854,13 @@ exports[`ConsultationListView component > renders as expected 1`] = ` class="pr-0 text-left col-4" >
Dec 1, 2024
@@ -775,7 +872,7 @@ exports[`ConsultationListView component > renders as expected 1`] = ` class="pr-0 text-left col-4" >
test comment
@@ -812,7 +909,7 @@ exports[`ConsultationListView component > renders as expected 1`] = `
renders as expected 1`] = ` class="px-1 col-auto" >
Jan 1, 2024
@@ -942,7 +1039,7 @@ exports[`ConsultationListView component > renders as expected 1`] = ` class="pr-0 text-left col-4" >
renders as expected 1`] = ` class="pr-0 text-left col-4" >
Yes
@@ -998,13 +1095,13 @@ exports[`ConsultationListView component > renders as expected 1`] = ` class="pr-0 text-left col-4" >
Dec 1, 2024
@@ -1016,7 +1113,7 @@ exports[`ConsultationListView component > renders as expected 1`] = ` class="pr-0 text-left col-4" >
test comment
@@ -1055,7 +1152,7 @@ exports[`ConsultationListView component > renders as expected 1`] = `

renders as expected 1`] = ` class="col" >
renders as expected 1`] = `

renders as expected 1`] = ` class="col" >
Date: Fri, 27 Sep 2024 11:50:32 -0700 Subject: [PATCH 4/4] PR corrections. --- .../detail/ConsultationListView.tsx | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/source/frontend/src/features/mapSideBar/lease/tabs/consultations/detail/ConsultationListView.tsx b/source/frontend/src/features/mapSideBar/lease/tabs/consultations/detail/ConsultationListView.tsx index b7212c40db..17481368a6 100644 --- a/source/frontend/src/features/mapSideBar/lease/tabs/consultations/detail/ConsultationListView.tsx +++ b/source/frontend/src/features/mapSideBar/lease/tabs/consultations/detail/ConsultationListView.tsx @@ -151,6 +151,21 @@ export const ConsultationListView: React.FunctionComponent } > + {consultation?.consultationTypeCode?.id === 'OTHER' && ( + + } + > + {consultation?.otherDescription} + + )} { return ( @@ -285,7 +300,7 @@ const getOutcomeIcon = (consultations: ApiGen_Concepts_ConsultationLease[]) => { return (