From 164317823244319d7eca41ca735f080e7a1f2fd8 Mon Sep 17 00:00:00 2001 From: Manuel Rodriguez Date: Fri, 23 Aug 2024 16:41:23 -0700 Subject: [PATCH 1/4] Added view and containers on the frontend for consultations. Updated backend for new specifications --- .../Controllers/ConsultationController.cs | 183 +++++++++++++++ source/backend/api/Services/ILeaseService.cs | 10 + source/backend/api/Services/LeaseService.cs | 53 ++++- .../Concepts/Deposit/SecurityDepositMap.cs | 2 - .../Concepts/Lease/ConsultationLeaseMap.cs | 33 ++- .../Concepts/Lease/ConsultationLeaseModel.cs | 30 ++- .../Extensions/ServiceCollectionExtensions.cs | 1 + .../Repositories/ConsultationRepository.cs | 92 ++++++++ .../Interfaces/IConsultationRepository.cs | 19 ++ .../Interfaces/ILeaseRepository.cs | 2 - .../dal/Repositories/LeaseRepository.cs | 25 -- .../Leases/LeaseImprovementControllerTest.cs | 7 - .../common/ContactFieldContainer.tsx | 71 ++++++ .../src/components/common/Section/Section.tsx | 2 +- .../common/Section/SectionField.tsx | 2 +- .../src/components/common/buttons/Button.tsx | 2 - .../stakeholders/AddLeaseStakeholderForm.tsx | 1 - .../stakeholders/ViewStakeholderForm.tsx | 1 - .../LeasePages/stakeholders/columns.tsx | 1 - .../add/AddAcquisitionAgreementContainer.tsx | 6 +- .../common/UpdateAcquisitionAgreementForm.tsx | 4 +- .../agreement/detail/AgreementContainer.tsx | 16 +- .../mapSideBar/lease/LeaseContainer.tsx | 11 + .../mapSideBar/lease/detail/LeaseFileTabs.tsx | 1 + .../lease/detail/LeaseTabsContainer.tsx | 13 + .../mapSideBar/lease/tabs/LeaseRouter.tsx | 82 +++++++ .../detail/ConsultationListContainer.tsx | 80 +++++++ .../detail/ConsultationListView.tsx | 222 ++++++++++++++++++ .../edit/ConsultationAddContainer.tsx | 69 ++++++ .../edit/ConsultationEditForm.tsx | 158 +++++++++++++ .../edit/ConsultationUpdateContainer.tsx | 124 ++++++++++ .../edit/EditConsultationYupSchema.ts | 14 ++ .../lease/tabs/consultations/edit/models.ts | 93 ++++++++ .../src/hooks/pims-api/useApiConsultations.ts | 44 ++++ .../repositories/useConsultationProvider.ts | 110 +++++++++ .../src/interfaces/IContactSearchResult.ts | 6 + .../ApiGen_Concepts_ConsultationLease.ts | 22 +- source/frontend/src/utils/formUtils.tsx | 10 +- source/frontend/src/utils/utils.ts | 8 + 39 files changed, 1558 insertions(+), 72 deletions(-) create mode 100644 source/backend/api/Areas/Leases/Controllers/ConsultationController.cs create mode 100644 source/backend/dal/Repositories/ConsultationRepository.cs create mode 100644 source/backend/dal/Repositories/Interfaces/IConsultationRepository.cs create mode 100644 source/frontend/src/components/common/ContactFieldContainer.tsx create mode 100644 source/frontend/src/features/mapSideBar/lease/tabs/LeaseRouter.tsx create mode 100644 source/frontend/src/features/mapSideBar/lease/tabs/consultations/detail/ConsultationListContainer.tsx create mode 100644 source/frontend/src/features/mapSideBar/lease/tabs/consultations/detail/ConsultationListView.tsx create mode 100644 source/frontend/src/features/mapSideBar/lease/tabs/consultations/edit/ConsultationAddContainer.tsx create mode 100644 source/frontend/src/features/mapSideBar/lease/tabs/consultations/edit/ConsultationEditForm.tsx create mode 100644 source/frontend/src/features/mapSideBar/lease/tabs/consultations/edit/ConsultationUpdateContainer.tsx create mode 100644 source/frontend/src/features/mapSideBar/lease/tabs/consultations/edit/EditConsultationYupSchema.ts create mode 100644 source/frontend/src/features/mapSideBar/lease/tabs/consultations/edit/models.ts create mode 100644 source/frontend/src/hooks/pims-api/useApiConsultations.ts create mode 100644 source/frontend/src/hooks/repositories/useConsultationProvider.ts diff --git a/source/backend/api/Areas/Leases/Controllers/ConsultationController.cs b/source/backend/api/Areas/Leases/Controllers/ConsultationController.cs new file mode 100644 index 0000000000..eef37601ce --- /dev/null +++ b/source/backend/api/Areas/Leases/Controllers/ConsultationController.cs @@ -0,0 +1,183 @@ + +using System; +using System.Collections.Generic; +using MapsterMapper; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Pims.Api.Helpers.Exceptions; +using Pims.Api.Models.Concepts.Lease; +using Pims.Api.Policies; +using Pims.Api.Services; +using Pims.Core.Extensions; +using Pims.Core.Json; +using Pims.Dal.Security; +using Swashbuckle.AspNetCore.Annotations; + +namespace Pims.Api.Areas.Lease.Controllers +{ + /// + /// ConsultationController class, provides endpoints for interacting with lease files consultation. + /// + [Authorize] + [ApiController] + [ApiVersion("1.0")] + [Area("leases")] + [Route("v{version:apiVersion}/[area]")] + [Route("[area]")] + public class ConsultationController : ControllerBase + { + #region Variables + private readonly ILeaseService _leaseService; + private readonly IMapper _mapper; + private readonly ILogger _logger; + + #endregion + + #region Constructors + + /// + /// Creates a new instance of a ConsultationController class, initializes it with the specified arguments. + /// + /// + /// + /// + public ConsultationController(ILeaseService leaseService, IMapper mapper, ILogger logger) + { + _leaseService = leaseService; + _mapper = mapper; + _logger = logger; + } + #endregion + + #region Endpoints + + /// + /// Get the lease file consultation. + /// + /// The consultation items. + [HttpGet("{id:long}/consultations")] + [HasPermission(Permissions.LeaseView)] + [Produces("application/json")] + [ProducesResponseType(typeof(IEnumerable), 200)] + [SwaggerOperation(Tags = new[] { "leasefile" })] + public IActionResult GetLeaseConsultations([FromRoute] long id) + { + _logger.LogInformation( + "Request received by Controller: {Controller}, Action: {ControllerAction}, User: {User}, DateTime: {DateTime}", + nameof(ConsultationController), + nameof(GetLeaseConsultations), + User.GetUsername(), + DateTime.Now); + + var consultation = _leaseService.GetConsultations(id); + return new JsonResult(_mapper.Map>(consultation)); + } + + /// + /// Create the lease file consultation to the lease file. + /// + /// The consultation items. + [HttpPost("{id:long}/consultations")] + [HasPermission(Permissions.LeaseView)] + [Produces("application/json")] + [ProducesResponseType(typeof(ConsultationLeaseModel), 201)] + [TypeFilter(typeof(NullJsonResultFilter))] + [SwaggerOperation(Tags = new[] { "leasefile" })] + public IActionResult AddLeaseConsultation([FromRoute] long id, [FromBody] ConsultationLeaseModel consultation) + { + _logger.LogInformation( + "Request received by Controller: {Controller}, Action: {ControllerAction}, User: {User}, DateTime: {DateTime}", + nameof(ConsultationController), + nameof(AddLeaseConsultation), + User.GetUsername(), + DateTime.Now); + + if (id != consultation.LeaseId) + { + throw new BadRequestException("Invalid LeaseId."); + } + + var newConsultation = _leaseService.AddConsultation(id, _mapper.Map(consultation)); + + return new JsonResult(_mapper.Map(newConsultation)); + } + + /// + /// Get the lease file consultation by Id. + /// + /// The consultation items. + [HttpGet("{id:long}/consultations/{consultationId:long}")] + [HasPermission(Permissions.LeaseView)] + [Produces("application/json")] + [ProducesResponseType(typeof(ConsultationLeaseModel), 200)] + [SwaggerOperation(Tags = new[] { "leasefile" })] + public IActionResult GetLeaseConsultationById([FromRoute]long id, [FromRoute]long consultationId) + { + _logger.LogInformation( + "Request received by Controller: {Controller}, Action: {ControllerAction}, User: {User}, DateTime: {DateTime}", + nameof(ConsultationController), + nameof(GetLeaseConsultationById), + User.GetUsername(), + DateTime.Now); + + var consultation = _leaseService.GetConsultationById(consultationId); + + return new JsonResult(_mapper.Map(consultation)); + } + + /// + /// Update the lease file consultation by Id. + /// + /// The consultation item updated. + [HttpPut("{id:long}/consultations/{consultationId:long}")] + [HasPermission(Permissions.LeaseEdit)] + [Produces("application/json")] + [ProducesResponseType(typeof(ConsultationLeaseModel), 200)] + [TypeFilter(typeof(NullJsonResultFilter))] + [SwaggerOperation(Tags = new[] { "leasefile" })] + public IActionResult UpdateLeaseConsultation([FromRoute] long id, [FromRoute] long consultationId, [FromBody] ConsultationLeaseModel consultation) + { + _logger.LogInformation( + "Request received by Controller: {Controller}, Action: {ControllerAction}, User: {User}, DateTime: {DateTime}", + nameof(ConsultationController), + nameof(UpdateLeaseConsultation), + User.GetUsername(), + DateTime.Now); + + if (id != consultation.LeaseId) + { + throw new BadRequestException("Invalid LeaseId."); + } + + var updatedConsultation = _leaseService.UpdateConsultation(_mapper.Map(consultation)); + + return new JsonResult(_mapper.Map(updatedConsultation)); + } + + /// + /// Delete the lease file consultation by Id. + /// + /// The consultation item updated. + [HttpDelete("{id:long}/consultations/{consultationId:long}")] + [HasPermission(Permissions.LeaseView)] + [Produces("application/json")] + [ProducesResponseType(typeof(bool), 200)] + [SwaggerOperation(Tags = new[] { "leasefile" })] + public IActionResult DeleteLeaseConsultation([FromRoute] long id, [FromRoute] long consultationId) + { + _logger.LogInformation( + "Request received by Controller: {Controller}, Action: {ControllerAction}, User: {User}, DateTime: {DateTime}", + nameof(ConsultationController), + nameof(DeleteLeaseConsultation), + User.GetUsername(), + DateTime.Now); + + var result = _leaseService.DeleteConsultation(consultationId); + + return new JsonResult(result); + } + + #endregion + } +} diff --git a/source/backend/api/Services/ILeaseService.cs b/source/backend/api/Services/ILeaseService.cs index cf23d6bd61..2fdd1cd74b 100644 --- a/source/backend/api/Services/ILeaseService.cs +++ b/source/backend/api/Services/ILeaseService.cs @@ -38,5 +38,15 @@ public interface ILeaseService PimsLease UpdateChecklistItems(long leaseId, IList checklistItems); IEnumerable GetAllStakeholderTypes(); + + IEnumerable GetConsultations(long leaseId); + + PimsLeaseConsultation GetConsultationById(long consultationId); + + PimsLeaseConsultation AddConsultation(long leaseId, PimsLeaseConsultation consultation); + + PimsLeaseConsultation UpdateConsultation(PimsLeaseConsultation consultation); + + bool DeleteConsultation(long consultationId); } } diff --git a/source/backend/api/Services/LeaseService.cs b/source/backend/api/Services/LeaseService.cs index 52b8a3e871..0be75e3207 100644 --- a/source/backend/api/Services/LeaseService.cs +++ b/source/backend/api/Services/LeaseService.cs @@ -32,6 +32,7 @@ public class LeaseService : BaseService, ILeaseService private readonly IInsuranceRepository _insuranceRepository; private readonly ILeaseStakeholderRepository _stakeholderRepository; private readonly ILeaseRenewalRepository _renewalRepository; + private readonly IConsultationRepository _consultationRepository; private readonly IUserRepository _userRepository; private readonly IPropertyService _propertyService; private readonly ILookupRepository _lookupRepository; @@ -48,6 +49,7 @@ public LeaseService( IInsuranceRepository insuranceRepository, ILeaseStakeholderRepository stakeholderRepository, ILeaseRenewalRepository renewalRepository, + IConsultationRepository consultationRepository, IUserRepository userRepository, IPropertyService propertyService, ILookupRepository lookupRepository) @@ -63,6 +65,7 @@ public LeaseService( _insuranceRepository = insuranceRepository; _stakeholderRepository = stakeholderRepository; _renewalRepository = renewalRepository; + _consultationRepository = consultationRepository; _userRepository = userRepository; _propertyService = propertyService; _lookupRepository = lookupRepository; @@ -254,8 +257,6 @@ public PimsLease Update(PimsLease lease, IEnumerable userOverr _propertyLeaseRepository.UpdatePropertyLeases(lease.Internal_Id, leaseWithProperties.PimsPropertyLeases); - _leaseRepository.UpdateLeaseConsultations(lease.Internal_Id, lease.ConcurrencyControlNumber, lease.PimsLeaseConsultations); - _leaseRepository.UpdateLeaseRenewals(lease.Internal_Id, lease.ConcurrencyControlNumber, lease.PimsLeaseRenewals); List differenceSet = currentFileProperties.Where(x => !lease.PimsPropertyLeases.Any(y => y.Internal_Id == x.Internal_Id)).ToList(); @@ -340,6 +341,54 @@ public IEnumerable GetAllStakeholderTypes() return _leaseRepository.GetAllLeaseStakeholderTypes(); } + public IEnumerable GetConsultations(long leaseId) + { + _logger.LogInformation("Getting lease consultations with Lease id: {leaseId}", leaseId); + _user.ThrowIfNotAuthorized(Permissions.LeaseView); + + return _consultationRepository.GetConsultationsByLease(leaseId); + } + + public PimsLeaseConsultation GetConsultationById(long consultationId) + { + _logger.LogInformation("Getting consultation with id: {consultationId}", consultationId); + _user.ThrowIfNotAuthorized(Permissions.LeaseEdit); + + return _consultationRepository.GetConsultationById(consultationId); + } + + public PimsLeaseConsultation AddConsultation(long leaseId, PimsLeaseConsultation consultation) + { + _user.ThrowIfNotAuthorized(Permissions.LeaseEdit); + + var newConsultation = _consultationRepository.AddConsultation(consultation); + _consultationRepository.CommitTransaction(); + + return newConsultation; + } + + public PimsLeaseConsultation UpdateConsultation(PimsLeaseConsultation consultation) + { + _user.ThrowIfNotAuthorized(Permissions.LeaseEdit); + + var currentConsultation = _consultationRepository.GetConsultationById(consultation.LeaseConsultationId); + + var updatedConsultation = _consultationRepository.UpdateConsultation(consultation); + _consultationRepository.CommitTransaction(); + + return updatedConsultation; + } + + public bool DeleteConsultation(long consultationId) + { + _user.ThrowIfNotAuthorized(Permissions.LeaseEdit); + + bool deleteResult = _consultationRepository.TryDeleteConsultation(consultationId); + _consultationRepository.CommitTransaction(); + + return deleteResult; + } + private static void ValidateRenewalDates(PimsLease lease, ICollection renewals) { if (lease.LeaseStatusTypeCode != PimsLeaseStatusTypes.ACTIVE) diff --git a/source/backend/apimodels/Models/Concepts/Deposit/SecurityDepositMap.cs b/source/backend/apimodels/Models/Concepts/Deposit/SecurityDepositMap.cs index ad76073d86..b4cfd22af0 100644 --- a/source/backend/apimodels/Models/Concepts/Deposit/SecurityDepositMap.cs +++ b/source/backend/apimodels/Models/Concepts/Deposit/SecurityDepositMap.cs @@ -1,7 +1,5 @@ -using System; using Mapster; using Pims.Api.Models.Base; -using Pims.Core.Extensions; using Entity = Pims.Dal.Entities; namespace Pims.Api.Models.Concepts.Deposit diff --git a/source/backend/apimodels/Models/Concepts/Lease/ConsultationLeaseMap.cs b/source/backend/apimodels/Models/Concepts/Lease/ConsultationLeaseMap.cs index 61834a9962..43198aee82 100644 --- a/source/backend/apimodels/Models/Concepts/Lease/ConsultationLeaseMap.cs +++ b/source/backend/apimodels/Models/Concepts/Lease/ConsultationLeaseMap.cs @@ -1,5 +1,6 @@ using Mapster; using Pims.Api.Models.Base; +using Pims.Core.Extensions; using Pims.Dal.Entities; namespace Pims.Api.Models.Concepts.Lease @@ -10,18 +11,38 @@ public void Register(TypeAdapterConfig config) { config.NewConfig() .Map(dest => dest.Id, src => src.LeaseConsultationId) - .Map(dest => dest.ConsultationType, src => src.ConsultationTypeCodeNavigation) - .Map(dest => dest.ConsultationStatusType, src => src.ConsultationStatusTypeCodeNavigation) - .Map(dest => dest.ParentLeaseId, src => src.LeaseId) + .Map(dest => dest.LeaseId, src => src.LeaseId) + .Map(dest => dest.Lease, src => src.Lease) + .Map(dest => dest.PersonId, src => src.PersonId) + .Map(dest => dest.Person, src => src.Person) + .Map(dest => dest.OrganizationId, src => src.OrganizationId) + .Map(dest => dest.Organization, src => src.Organization) + .Map(dest => dest.PrimaryContactId, src => src.PrimaryContactId) + .Map(dest => dest.PrimaryContact, src => src.PrimaryContact) + .Map(dest => dest.ConsultationTypeCode, src => src.ConsultationTypeCodeNavigation) + .Map(dest => dest.ConsultationStatusTypeCode, src => src.ConsultationStatusTypeCodeNavigation) .Map(dest => dest.OtherDescription, src => src.OtherDescription) + .Map(dest => dest.RequestedOn, src => src.RequestedOn.ToNullableDateOnly()) + .Map(dest => dest.IsResponseReceived, src => src.IsResponseReceived) + .Map(dest => dest.ResponseReceivedDate, src => src.ResponseReceivedDate.ToNullableDateOnly()) + .Map(dest => dest.Comment, src => src.Comment) .Inherits(); config.NewConfig() .Map(dest => dest.LeaseConsultationId, src => src.Id) - .Map(dest => dest.LeaseId, src => src.ParentLeaseId) - .Map(dest => dest.ConsultationTypeCode, src => src.ConsultationType.Id) - .Map(dest => dest.ConsultationStatusTypeCode, src => src.ConsultationStatusType.Id) + .Map(dest => dest.LeaseId, src => src.LeaseId) + .Map(dest => dest.PersonId, src => src.PersonId) + .Map(dest => dest.OrganizationId, src => src.OrganizationId) + .Map(dest => dest.Organization, src => src.Organization) + .Map(dest => dest.PrimaryContactId, src => src.PrimaryContactId) + .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.OtherDescription, src => src.OtherDescription) + .Map(dest => dest.RequestedOn, src => src.RequestedOn.ToNullableDateTime()) + .Map(dest => dest.IsResponseReceived, src => src.IsResponseReceived) + .Map(dest => dest.ResponseReceivedDate, src => src.ResponseReceivedDate.ToNullableDateTime()) + .Map(dest => dest.Comment, src => src.Comment) .Inherits(); } } diff --git a/source/backend/apimodels/Models/Concepts/Lease/ConsultationLeaseModel.cs b/source/backend/apimodels/Models/Concepts/Lease/ConsultationLeaseModel.cs index daf6a0dec7..1038a29860 100644 --- a/source/backend/apimodels/Models/Concepts/Lease/ConsultationLeaseModel.cs +++ b/source/backend/apimodels/Models/Concepts/Lease/ConsultationLeaseModel.cs @@ -1,4 +1,7 @@ +using System; using Pims.Api.Models.Base; +using Pims.Api.Models.Concepts.Organization; +using Pims.Api.Models.Concepts.Person; namespace Pims.Api.Models.Concepts.Lease { @@ -8,14 +11,35 @@ public class ConsultationLeaseModel : BaseAuditModel public long Id { get; set; } - public CodeTypeModel ConsultationType { get; set; } + public long LeaseId { get; set; } - public CodeTypeModel ConsultationStatusType { get; set; } + public LeaseModel Lease { get; set; } - public long ParentLeaseId { get; set; } + public long? PersonId { get; set; } + + public PersonModel Person { get; set; } + + public long? OrganizationId { get; set; } + + public OrganizationModel Organization { get; set; } + + public long? PrimaryContactId { get; set; } + + public PersonModel PrimaryContact { get; set; } + + public CodeTypeModel ConsultationTypeCode { get; set; } + + public CodeTypeModel ConsultationStatusTypeCode { get; set; } public string OtherDescription { get; set; } + public DateOnly? RequestedOn { get; set; } + + public bool? IsResponseReceived { get; set; } + + public DateOnly? ResponseReceivedDate { get; set; } + + public string Comment { get; set; } #endregion } } diff --git a/source/backend/dal/Helpers/Extensions/ServiceCollectionExtensions.cs b/source/backend/dal/Helpers/Extensions/ServiceCollectionExtensions.cs index a0bee4f158..75511b9f23 100644 --- a/source/backend/dal/Helpers/Extensions/ServiceCollectionExtensions.cs +++ b/source/backend/dal/Helpers/Extensions/ServiceCollectionExtensions.cs @@ -83,6 +83,7 @@ public static IServiceCollection AddPimsDalRepositories(this IServiceCollection repositories.AddScoped(); repositories.AddScoped(); repositories.AddScoped(); + repositories.AddScoped(); return repositories; } diff --git a/source/backend/dal/Repositories/ConsultationRepository.cs b/source/backend/dal/Repositories/ConsultationRepository.cs new file mode 100644 index 0000000000..a40d99a95a --- /dev/null +++ b/source/backend/dal/Repositories/ConsultationRepository.cs @@ -0,0 +1,92 @@ + +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Pims.Dal.Entities; +using Pims.Dal.Helpers.Extensions; + +namespace Pims.Dal.Repositories +{ + /// + /// Provides a repository to interact with consultations within the datasource. + /// + public class ConsultationRepository : BaseRepository, IConsultationRepository + { + #region Constructors + + /// + /// Creates a new instance of a ConsultationRepository, and initializes it with the specified arguments. + /// + /// + /// + /// + public ConsultationRepository(PimsContext dbContext, ClaimsPrincipal user, ILogger logger) + : base(dbContext, user, logger) + { + } + + #endregion + + #region Methods + + public List GetConsultationsByLease(long leaseId) + { + using var scope = Logger.QueryScope(); + + return Context.PimsLeaseConsultations + .Where(lc => lc.LeaseId == leaseId) + .Include(lc => lc.ConsultationTypeCodeNavigation) + .AsNoTracking() + .ToList(); + } + + public PimsLeaseConsultation AddConsultation(PimsLeaseConsultation consultation) + { + using var scope = Logger.QueryScope(); + + Context.PimsLeaseConsultations.Add(consultation); + + return consultation; + } + + public PimsLeaseConsultation GetConsultationById(long consultationId) + { + using var scope = Logger.QueryScope(); + + return Context.PimsLeaseConsultations.Where(x => x.LeaseConsultationId == consultationId) + .AsNoTracking() + .Include(x => x.ConsultationTypeCodeNavigation) + .FirstOrDefault() ?? throw new KeyNotFoundException(); + } + + public PimsLeaseConsultation UpdateConsultation(PimsLeaseConsultation consultation) + { + using var scope = Logger.QueryScope(); + + var existingConsultation = Context.PimsLeaseConsultations.FirstOrDefault(x => x.LeaseConsultationId == consultation.LeaseConsultationId) ?? throw new KeyNotFoundException(); + + Context.Entry(existingConsultation).CurrentValues.SetValues(consultation); + + return existingConsultation; + } + + public bool TryDeleteConsultation(long consultationId) + { + using var scope = Logger.QueryScope(); + + var deletedEntity = Context.PimsLeaseConsultations.Where(x => x.LeaseConsultationId == consultationId).FirstOrDefault(); + if (deletedEntity is not null) + { + Context.PimsLeaseConsultations.Remove(deletedEntity); + + return true; + } + + return false; + } + + #endregion + } +} diff --git a/source/backend/dal/Repositories/Interfaces/IConsultationRepository.cs b/source/backend/dal/Repositories/Interfaces/IConsultationRepository.cs new file mode 100644 index 0000000000..b8acb2632c --- /dev/null +++ b/source/backend/dal/Repositories/Interfaces/IConsultationRepository.cs @@ -0,0 +1,19 @@ + +using System.Collections.Generic; +using Pims.Dal.Entities; + +namespace Pims.Dal.Repositories +{ + public interface IConsultationRepository : IRepository + { + List GetConsultationsByLease(long leaseId); + + PimsLeaseConsultation GetConsultationById(long consultationId); + + PimsLeaseConsultation AddConsultation(PimsLeaseConsultation consultation); + + PimsLeaseConsultation UpdateConsultation(PimsLeaseConsultation consultation); + + bool TryDeleteConsultation(long consultationId); + } +} diff --git a/source/backend/dal/Repositories/Interfaces/ILeaseRepository.cs b/source/backend/dal/Repositories/Interfaces/ILeaseRepository.cs index b140b4daa5..7b47952afe 100644 --- a/source/backend/dal/Repositories/Interfaces/ILeaseRepository.cs +++ b/source/backend/dal/Repositories/Interfaces/ILeaseRepository.cs @@ -33,8 +33,6 @@ public interface ILeaseRepository : IRepository PimsLease Update(PimsLease lease, bool commitTransaction = true); - PimsLease UpdateLeaseConsultations(long leaseId, long? rowVersion, ICollection pimsLeaseConsultations); - PimsLease UpdateLeaseRenewals(long leaseId, long? rowVersion, ICollection renewals); IEnumerable GetAllChecklistItemTypes(); diff --git a/source/backend/dal/Repositories/LeaseRepository.cs b/source/backend/dal/Repositories/LeaseRepository.cs index cdf5c620dc..87aec9accd 100644 --- a/source/backend/dal/Repositories/LeaseRepository.cs +++ b/source/backend/dal/Repositories/LeaseRepository.cs @@ -97,10 +97,6 @@ public PimsLease Get(long id) .Include(l => l.PimsInsurances) .Include(l => l.PimsSecurityDeposits) .Include(l => l.PimsLeasePeriods) - .Include(l => l.PimsLeaseConsultations) - .ThenInclude(lc => lc.ConsultationStatusTypeCodeNavigation) - .Include(l => l.PimsLeaseConsultations) - .ThenInclude(lc => lc.ConsultationTypeCodeNavigation) .Include(l => l.Project) .FirstOrDefault(l => l.LeaseId == id) ?? throw new KeyNotFoundException(); @@ -838,27 +834,6 @@ public PimsLease Update(PimsLease lease, bool commitTransaction = true) return existingLease; } - /// - /// Update the consultations of a lease. - /// - /// - /// - /// - /// - public PimsLease UpdateLeaseConsultations(long leaseId, long? rowVersion, ICollection pimsLeaseConsultations) - { - var existingLease = this.Context.PimsLeases.Include(l => l.PimsLeaseConsultations).AsNoTracking().FirstOrDefault(l => l.LeaseId == leaseId) - ?? throw new KeyNotFoundException(); - if (existingLease.ConcurrencyControlNumber != rowVersion) - { - throw new DbUpdateConcurrencyException("Unable to save. Please refresh your page and try again"); - } - - this.Context.UpdateChild(l => l.PimsLeaseConsultations, leaseId, pimsLeaseConsultations.ToArray()); - - return GetNoTracking(existingLease.LeaseId); - } - /// /// Update the renewals of a lease. /// diff --git a/source/backend/tests/unit/api/Controllers/Leases/LeaseImprovementControllerTest.cs b/source/backend/tests/unit/api/Controllers/Leases/LeaseImprovementControllerTest.cs index 863f23c005..8c8266de65 100644 --- a/source/backend/tests/unit/api/Controllers/Leases/LeaseImprovementControllerTest.cs +++ b/source/backend/tests/unit/api/Controllers/Leases/LeaseImprovementControllerTest.cs @@ -1,18 +1,11 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; -using FluentAssertions; using MapsterMapper; -using Microsoft.AspNetCore.Mvc; using Moq; -using Pims.Api.Areas.Acquisition.Controllers; using Pims.Api.Areas.Lease.Controllers; using Pims.Api.Models.Concepts.Lease; using Pims.Api.Services; using Pims.Core.Test; -using Pims.Dal; -using Pims.Dal.Entities; -using Pims.Dal.Repositories; using Pims.Dal.Security; using Xunit; diff --git a/source/frontend/src/components/common/ContactFieldContainer.tsx b/source/frontend/src/components/common/ContactFieldContainer.tsx new file mode 100644 index 0000000000..8ac326963f --- /dev/null +++ b/source/frontend/src/components/common/ContactFieldContainer.tsx @@ -0,0 +1,71 @@ +import { useCallback, useEffect, useState } from 'react'; + +import { useOrganizationRepository } from '@/features/contacts/repositories/useOrganizationRepository'; +import { usePersonRepository } from '@/features/contacts/repositories/usePersonRepository'; +import { ApiGen_Concepts_Organization } from '@/models/api/generated/ApiGen_Concepts_Organization'; +import { ApiGen_Concepts_Person } from '@/models/api/generated/ApiGen_Concepts_Person'; +import { exists, isValidId } from '@/utils'; + +import ContactLink from './ContactLink'; +import { ISectionFieldProps, SectionField } from './Section/SectionField'; + +interface IContactFieldContainerProps extends ISectionFieldProps { + personId: number | null; + organizationId: number | null; + primaryContact: number | null; +} + +export const ContactFieldContainer: React.FunctionComponent< + React.PropsWithChildren +> = props => { + const [person, setPerson] = useState(null); + const [organization, setOrganization] = useState(null); + const [primaryContact, setPrimaryContact] = useState(null); + + const { + getPersonDetail: { execute: getPerson, loading: getPersonLoading }, + } = usePersonRepository(); + + const { + getOrganizationDetail: { execute: getOrganization, loading: getOrganizationLoading }, + } = useOrganizationRepository(); + + const fetchData = useCallback(async () => { + if (isValidId(props.personId)) { + const returnedPerson = await getPerson(props.personId); + if (exists(returnedPerson)) { + setPerson(returnedPerson); + } + } + if (isValidId(props.organizationId)) { + const returnedOrganization = await getOrganization(props.organizationId); + if (exists(returnedOrganization)) { + setOrganization(returnedOrganization); + } + } + + if (isValidId(props.primaryContact)) { + const returnedPrimaryContact = await getPerson(props.primaryContact); + if (exists(returnedPrimaryContact)) { + setPrimaryContact(returnedPrimaryContact); + } + } + }, [getOrganization, getPerson, props.organizationId, props.personId, props.primaryContact]); + + useEffect(() => { + fetchData(); + }, [fetchData]); + if (getPersonLoading || getOrganizationLoading) { + return <>; + } + + return ( + + {exists(person) && } + {exists(organization) && } + {exists(primaryContact) && } + + ); +}; + +export default ContactFieldContainer; diff --git a/source/frontend/src/components/common/Section/Section.tsx b/source/frontend/src/components/common/Section/Section.tsx index 37f0e0ec0b..5d0e7b2dbc 100644 --- a/source/frontend/src/components/common/Section/Section.tsx +++ b/source/frontend/src/components/common/Section/Section.tsx @@ -30,7 +30,7 @@ export const Section: React.FC< className, ...rest }) => { - const [isCollapsed, setIsCollapsed] = useState(!initiallyExpanded && true); + const [isCollapsed, setIsCollapsed] = useState(!(initiallyExpanded === true)); return (
> = ({ nameSpace, stakeholders, loading, leaseStakeholderTypes, isPayableLease }) => { - console.log(leaseStakeholderTypes, isPayableLease); return ( [] => { const stakeholderType = isPayableLease ? 'Payee type' : 'Contact type'; - console.log(stakeholderTypes); return [ { Header: ( diff --git a/source/frontend/src/features/mapSideBar/acquisition/tabs/agreement/add/AddAcquisitionAgreementContainer.tsx b/source/frontend/src/features/mapSideBar/acquisition/tabs/agreement/add/AddAcquisitionAgreementContainer.tsx index 12748b4d67..ab93e611e6 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/tabs/agreement/add/AddAcquisitionAgreementContainer.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/tabs/agreement/add/AddAcquisitionAgreementContainer.tsx @@ -7,12 +7,12 @@ import { useAgreementProvider } from '@/hooks/repositories/useAgreementProvider' import { useModalContext } from '@/hooks/useModalContext'; import { IApiError } from '@/interfaces/IApiError'; -import { IUpdateAcquisitionAgreementViewProps } from '../common/UpdateAcquisitionAgreementForm'; +import { IUpdateAcquisitionAgreementFormProps } from '../common/UpdateAcquisitionAgreementForm'; import { AcquisitionAgreementFormModel } from '../models/AcquisitionAgreementFormModel'; export interface IAddAcquisitionAgreementContainerProps { acquisitionFileId: number; - View: React.FC; + View: React.FC; onSuccess: () => void; } @@ -76,7 +76,7 @@ const AddAcquisitionAgreementContainer: React.FunctionComponent< isLoading={loading} onSubmit={handleSubmit} onCancel={() => history.push(backUrl)} - > + /> ) ); }; diff --git a/source/frontend/src/features/mapSideBar/acquisition/tabs/agreement/common/UpdateAcquisitionAgreementForm.tsx b/source/frontend/src/features/mapSideBar/acquisition/tabs/agreement/common/UpdateAcquisitionAgreementForm.tsx index b15d17cc2a..59057a622d 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/tabs/agreement/common/UpdateAcquisitionAgreementForm.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/tabs/agreement/common/UpdateAcquisitionAgreementForm.tsx @@ -10,7 +10,7 @@ import AcquisitionAgreementForm from '../form/AcquisitionAgreementForm'; import { AcquisitionAgreementFormYupSchema } from '../form/AcquisitionAgreementFormYupSchema'; import { AcquisitionAgreementFormModel } from '../models/AcquisitionAgreementFormModel'; -export interface IUpdateAcquisitionAgreementViewProps { +export interface IUpdateAcquisitionAgreementFormProps { isLoading: boolean; initialValues: AcquisitionAgreementFormModel | null; onSubmit: ( @@ -21,7 +21,7 @@ export interface IUpdateAcquisitionAgreementViewProps { } const UpdateAcquisitionAgreementForm: React.FunctionComponent< - React.PropsWithChildren + React.PropsWithChildren > = ({ isLoading, initialValues, onSubmit, onCancel }) => { const { setModalContent, setDisplayModal } = useModalContext(); diff --git a/source/frontend/src/features/mapSideBar/acquisition/tabs/agreement/detail/AgreementContainer.tsx b/source/frontend/src/features/mapSideBar/acquisition/tabs/agreement/detail/AgreementContainer.tsx index 3ac9966807..d7574d809c 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/tabs/agreement/detail/AgreementContainer.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/tabs/agreement/detail/AgreementContainer.tsx @@ -75,15 +75,13 @@ export const AgreementContainer: React.FunctionComponent< }, [fetchData]); return file?.id ? ( - <> - - + ) : null; }; diff --git a/source/frontend/src/features/mapSideBar/lease/LeaseContainer.tsx b/source/frontend/src/features/mapSideBar/lease/LeaseContainer.tsx index 9a709ef299..6f6a36cd89 100644 --- a/source/frontend/src/features/mapSideBar/lease/LeaseContainer.tsx +++ b/source/frontend/src/features/mapSideBar/lease/LeaseContainer.tsx @@ -30,6 +30,7 @@ import SidebarFooter from '../shared/SidebarFooter'; import { StyledFormWrapper } from '../shared/styles'; import LeaseHeader from './common/LeaseHeader'; import { LeaseFileTabNames } from './detail/LeaseFileTabs'; +import LeaseRouter from './tabs/LeaseRouter'; import ViewSelector from './ViewSelector'; export interface ILeaseContainerProps { @@ -82,6 +83,7 @@ export enum LeasePageNames { SURPLUS = 'surplus', CHECKLIST = 'checklist', DOCUMENTS = 'documents', + CONSULTATIONS = 'consultations', } export const leasePages: Map> = new Map< @@ -156,6 +158,15 @@ export const leasePages: Map> = new Map< claims: Claims.DOCUMENT_VIEW, }, ], + [ + LeasePageNames.CONSULTATIONS, + { + pageName: LeasePageNames.CONSULTATIONS, + component: LeaseRouter, + title: 'Consultations', + claims: Claims.LEASE_VIEW, + }, + ], ]); export const LeaseContainer: React.FC = ({ leaseId, onClose }) => { diff --git a/source/frontend/src/features/mapSideBar/lease/detail/LeaseFileTabs.tsx b/source/frontend/src/features/mapSideBar/lease/detail/LeaseFileTabs.tsx index 379def23fd..27f44293eb 100644 --- a/source/frontend/src/features/mapSideBar/lease/detail/LeaseFileTabs.tsx +++ b/source/frontend/src/features/mapSideBar/lease/detail/LeaseFileTabs.tsx @@ -18,6 +18,7 @@ interface ILeaseFileTabsProps { export enum LeaseFileTabNames { fileDetails = 'fileDetails', + consultations = 'consultations', tenant = 'tenant', payee = 'payee', improvements = 'improvements', diff --git a/source/frontend/src/features/mapSideBar/lease/detail/LeaseTabsContainer.tsx b/source/frontend/src/features/mapSideBar/lease/detail/LeaseTabsContainer.tsx index d18c247842..afc5fe4fda 100644 --- a/source/frontend/src/features/mapSideBar/lease/detail/LeaseTabsContainer.tsx +++ b/source/frontend/src/features/mapSideBar/lease/detail/LeaseTabsContainer.tsx @@ -52,6 +52,19 @@ export const LeaseTabsContainer: React.FC = ({ name: 'File Details', }); + tabViews.push({ + content: ( + + ), + key: LeaseFileTabNames.consultations, + name: 'Consultations', + }); + tabViews.push({ content: ( { + isEditing: boolean; + onEdit?: (isEditing: boolean) => void; + formikRef: React.RefObject>; + onSuccess: () => void; + componentView: React.FunctionComponent>; +} + +export const LeaseRouter: React.FunctionComponent> = ({ + onSuccess, +}) => { + const { lease } = useContext(LeaseStateContext); + const { path } = useRouteMatch(); + + if (!exists(lease)) { + return null; + } + + const consultationsPath = LeasePageNames.CONSULTATIONS; + return ( + + ( + + )} + claim={Claims.LEASE_VIEW} + key={'consultation'} + title="Lease Consultation" + /> + ( + + )} + claim={Claims.LEASE_EDIT} + key={'consultation'} + title="Add Lease Consultation" + /> + ( + + )} + claim={Claims.LEASE_EDIT} + key={'consultation'} + title="Edit Lease Consultation" + /> + + ); +}; + +export default LeaseRouter; 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 new file mode 100644 index 0000000000..981fa20d1a --- /dev/null +++ b/source/frontend/src/features/mapSideBar/lease/tabs/consultations/detail/ConsultationListContainer.tsx @@ -0,0 +1,80 @@ +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useHistory, useRouteMatch } from 'react-router-dom'; + +import { useConsultationProvider } from '@/hooks/repositories/useConsultationProvider'; +import { ApiGen_Concepts_ConsultationLease } from '@/models/api/generated/ApiGen_Concepts_ConsultationLease'; +import { isValidId } from '@/utils'; + +import { LeasePageNames } from '../../../LeaseContainer'; +import { IConsultationListViewProps } from './ConsultationListView'; + +interface IConsultationListProps { + leaseId: number; + View: React.FunctionComponent>; +} + +const ConsultationListContainer: React.FunctionComponent< + React.PropsWithChildren +> = ({ leaseId, View }) => { + const [leaseConsultations, setLeaseConsultations] = useState( + [], + ); + + const { + getLeaseConsultations: { execute: getConsultations, loading: loadingConsultations }, + deleteLeaseConsultation: { execute: deleteConsultation, loading: deletingConsultation }, + } = useConsultationProvider(); + + if (!isValidId(leaseId)) { + throw new Error('Unable to determine id of current file.'); + } + + const fetchData = useCallback(async () => { + const consultations = await getConsultations(leaseId); + + if (consultations) { + setLeaseConsultations(consultations); + } + }, [leaseId, getConsultations]); + + const history = useHistory(); + const match = useRouteMatch(); + + const handleConsultationAdd = async () => { + history.push(`${match.url}/${LeasePageNames.CONSULTATIONS}/add`); + }; + + const handleConsultationEdit = async (consultationId: number) => { + history.push(`${match.url}/${LeasePageNames.CONSULTATIONS}/${consultationId}/edit`); + }; + + const handleConsultationDeleted = async (consultationId: number) => { + if (isValidId(consultationId)) { + await deleteConsultation(leaseId, consultationId); + const updatedConsultations = await getConsultations(leaseId); + if (updatedConsultations) { + setLeaseConsultations(updatedConsultations); + } + } + }; + + useEffect(() => { + fetchData(); + }, [fetchData]); + + const isLoading = useMemo(() => { + return loadingConsultations || deletingConsultation; + }, [deletingConsultation, loadingConsultations]); + + return leaseId ? ( + + ) : null; +}; + +export default ConsultationListContainer; 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 new file mode 100644 index 0000000000..64cfc0acc5 --- /dev/null +++ b/source/frontend/src/features/mapSideBar/lease/tabs/consultations/detail/ConsultationListView.tsx @@ -0,0 +1,222 @@ +import { useMemo } from 'react'; +import { Col, Row } from 'react-bootstrap'; +import { FaPlus, FaTrash } from 'react-icons/fa'; +import { useHistory, useRouteMatch } from 'react-router-dom'; +import styled from 'styled-components'; + +import { StyledRemoveLinkButton } from '@/components/common/buttons/RemoveButton'; +import ContactFieldContainer from '@/components/common/ContactFieldContainer'; +import EditButton from '@/components/common/EditButton'; +import LoadingBackdrop from '@/components/common/LoadingBackdrop'; +import { Section } from '@/components/common/Section/Section'; +import { SectionField } from '@/components/common/Section/SectionField'; +import { StyledSummarySection } from '@/components/common/Section/SectionStyles'; +import { SectionListHeader } from '@/components/common/SectionListHeader'; +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_Concepts_ConsultationLease } from '@/models/api/generated/ApiGen_Concepts_ConsultationLease'; +import { prettyFormatDate } from '@/utils'; +import { booleanToYesNoUnknownString } from '@/utils/formUtils'; + +export interface IConsultationListViewProps { + loading: boolean; + consultations: ApiGen_Concepts_ConsultationLease[]; + onAdd: () => void; + onEdit: (consultationId: number) => void; + onDelete: (consultationId: number) => void; +} + +interface GroupedConsultations { + consultationTypeCode: string; + consultationTypeDescription: string; + consultations: ApiGen_Concepts_ConsultationLease[]; + hasItems: boolean; +} + +export const ConsultationListView: React.FunctionComponent = ({ + loading, + consultations, + onAdd, + onEdit, + onDelete, +}) => { + const keycloak = useKeycloakWrapper(); + const history = useHistory(); + const match = useRouteMatch(); + const { setModalContent, setDisplayModal } = useModalContext(); + + const { getOptionsByType } = useLookupCodeHelpers(); + const consultationTypeCodes = getOptionsByType(API.CONSULTATION_TYPES); + + const groupedConsultations = useMemo(() => { + const grouped: GroupedConsultations[] = []; + consultationTypeCodes.forEach(ct => + grouped.push({ + consultationTypeCode: ct.value.toString(), + consultationTypeDescription: ct.label, + consultations: consultations.filter(c => c.consultationTypeCode.id === ct.value), + hasItems: consultations.filter(c => c.consultationTypeCode.id === ct.value).length === 0, + }), + ); + return grouped; + }, [consultationTypeCodes, consultations]); + + if (loading) { + return ; + } + + return ( + +
} + onAdd={onAdd} + /> + } + > + {groupedConsultations.map((group, index) => ( +
+ + {group.consultationTypeDescription} + + + {group.consultations.length > 0 && ( + {group.consultations.length} + )} + + + } + noPadding + isCollapsable={!group.hasItems} + initiallyExpanded={group.hasItems} + > + {group.consultations.map((consultation, index) => ( + +
+ + + {group.consultationTypeCode === 'OTHER' + ? `${consultation.otherDescription} Consultation` + : `${group.consultationTypeDescription} Consultation`} + + + {keycloak.hasClaim(Claims.LEASE_EDIT) && ( + <> + + { + setModalContent({ + ...getDeleteModalProps(), + variant: 'error', + title: 'Delete Consultation', + message: `You have selected to delete this Consultation. + Do you want to proceed?`, + okButtonText: 'Yes', + cancelButtonText: 'No', + handleOk: async () => { + consultation.id && onDelete(consultation.id); + setDisplayModal(false); + }, + handleCancel: () => { + setDisplayModal(false); + }, + }); + setDisplayModal(true); + }} + > + + + + + onEdit(consultation.id)} + /> + + + )} + + + } + > + + {prettyFormatDate(consultation.requestedOn)} + + + + {booleanToYesNoUnknownString(consultation.isResponseReceived)} + + + {prettyFormatDate(consultation.responseReceivedDate)} + + + {consultation.comment} + + + {consultation.consultationTypeCode.id} + +
+
+ ))} + {group.consultations.length === 0 && ( +

There are no consultations.

+ )} +
+ ))} +
+
+ ); +}; + +export default ConsultationListView; + +export const StyledButtonContainer = styled.div` + display: flex; + flex-direction: row; + justify-content: flex-end; + align-items: center; + margin-bottom: 0.5rem; + align-items: center; +`; + +const StyledBorder = styled.div` + border: solid 0.2rem ${props => props.theme.css.headerBorderColor}; + margin-bottom: 1.5rem; + border-radius: 0.5rem; +`; + +const StyledIconWrapper = styled.div` + background-color: ${props => props.theme.css.activeActionColor}; + color: white; + font-size: 1.4rem; + border-radius: 50%; + opacity: 0.8; + width: 2.2rem; + height: 2.2rem; + padding: 1rem; + display: flex; + justify-content: center; + align-items: center; +`; diff --git a/source/frontend/src/features/mapSideBar/lease/tabs/consultations/edit/ConsultationAddContainer.tsx b/source/frontend/src/features/mapSideBar/lease/tabs/consultations/edit/ConsultationAddContainer.tsx new file mode 100644 index 0000000000..61b0f12433 --- /dev/null +++ b/source/frontend/src/features/mapSideBar/lease/tabs/consultations/edit/ConsultationAddContainer.tsx @@ -0,0 +1,69 @@ +import axios, { AxiosError } from 'axios'; +import { FormikHelpers } from 'formik'; +import { useHistory, useLocation } from 'react-router-dom'; +import { toast } from 'react-toastify'; + +import { useConsultationProvider } from '@/hooks/repositories/useConsultationProvider'; +import { IApiError } from '@/interfaces/IApiError'; + +import { LeasePageNames } from '../../../LeaseContainer'; +import { IConsultationEditFormProps } from './ConsultationEditForm'; +import { ConsultationFormModel } from './models'; + +export interface IConsultationAddProps { + leaseId: number; + onSuccess: () => void; + View: React.FunctionComponent>; +} + +const ConsultationAddContainer: React.FunctionComponent< + React.PropsWithChildren +> = ({ onSuccess, View, leaseId }) => { + const history = useHistory(); + const location = useLocation(); + + const backUrl = location.pathname.split(`/${LeasePageNames.CONSULTATIONS}/add`)[0]; + + const { + addLeaseConsultation: { execute: addConsultation, loading: addConsultationLoading }, + } = useConsultationProvider(); + + const onCreateError = (e: AxiosError) => { + if (e?.response?.status === 400) { + toast.error(e?.response.data.error); + } else { + toast.error('Unable to save. Please try again.'); + } + }; + + const handleSubmit = async ( + values: ConsultationFormModel, + formikHelpers: FormikHelpers, + ) => { + try { + const consultationSaved = await addConsultation(leaseId, values.toApi()); + if (consultationSaved) { + onSuccess(); + history.push(backUrl); + } + } catch (e) { + if (axios.isAxiosError(e)) { + const axiosError = e as AxiosError; + onCreateError && onCreateError(axiosError); + } + } finally { + formikHelpers.setSubmitting(false); + } + }; + + return ( + history.push(backUrl)} + /> + ); +}; + +export default ConsultationAddContainer; 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 new file mode 100644 index 0000000000..2573372081 --- /dev/null +++ b/source/frontend/src/features/mapSideBar/lease/tabs/consultations/edit/ConsultationEditForm.tsx @@ -0,0 +1,158 @@ +import { Formik, FormikHelpers } from 'formik'; +import styled from 'styled-components'; + +import { FastDatePicker, Input, Select, TextArea } from '@/components/common/form'; +import { ContactInputContainer } from '@/components/common/form/ContactInput/ContactInputContainer'; +import ContactInputView from '@/components/common/form/ContactInput/ContactInputView'; +import { PrimaryContactSelector } from '@/components/common/form/PrimaryContactSelector/PrimaryContactSelector'; +import { YesNoSelect } from '@/components/common/form/YesNoSelect'; +import LoadingBackdrop from '@/components/common/LoadingBackdrop'; +import { Section } from '@/components/common/Section/Section'; +import { SectionField } from '@/components/common/Section/SectionField'; +import { StyledDivider } from '@/components/common/styles'; +import * as API from '@/constants/API'; +import SidebarFooter from '@/features/mapSideBar/shared/SidebarFooter'; +import { StyledFormWrapper } from '@/features/mapSideBar/shared/styles'; +import useLookupCodeHelpers from '@/hooks/useLookupCodeHelpers'; +import { getCancelModalProps, useModalContext } from '@/hooks/useModalContext'; +import { isOrganizationResult } from '@/interfaces'; +import { exists, isValidId } from '@/utils'; + +import { UpdateConsultationYupSchema } from './EditConsultationYupSchema'; +import { ConsultationFormModel } from './models'; + +export interface IConsultationEditFormProps { + isLoading: boolean; + initialValues: ConsultationFormModel | null; + onSubmit: ( + values: ConsultationFormModel, + formikHelpers: FormikHelpers, + ) => Promise; + onCancel: () => void; +} + +export const ConsultationEditForm: React.FunctionComponent = ({ + isLoading, + initialValues, + onSubmit, + onCancel, +}) => { + const { setModalContent, setDisplayModal } = useModalContext(); + + const { getOptionsByType } = useLookupCodeHelpers(); + const consultationTypeCodes = getOptionsByType(API.CONSULTATION_TYPES); + + const cancelFunc = (resetForm: () => void, dirty: boolean) => { + if (!dirty) { + resetForm(); + onCancel(); + } else { + setModalContent({ + ...getCancelModalProps(), + handleOk: () => { + resetForm(); + setDisplayModal(false); + onCancel(); + }, + }); + setDisplayModal(true); + } + }; + + const headerTitle = !isValidId(initialValues.id) ? 'Add Consultation' : 'Update Consultation'; + + return ( + initialValues && ( + + + enableReinitialize + initialValues={initialValues} + validationSchema={UpdateConsultationYupSchema} + onSubmit={onSubmit} + > + {formikProps => { + return ( + <> + + +
+ + + + )} + + + + + + + {formikProps.values.isResponseReceived === true && ( + + + + )} + +