diff --git a/source/frontend/src/components/maps/MapLeafletView.tsx b/source/frontend/src/components/maps/MapLeafletView.tsx index d1df6c6c62..aa3da327eb 100644 --- a/source/frontend/src/components/maps/MapLeafletView.tsx +++ b/source/frontend/src/components/maps/MapLeafletView.tsx @@ -67,13 +67,13 @@ const MapLeafletView: React.FC> = ( const [activeFeatureLayer, setActiveFeatureLayer] = useState(); const { doubleClickInterval } = useTenant(); - const timer = useRef(null); - // add geojson layer to the map if (!!mapRef.current && !activeFeatureLayer) { setActiveFeatureLayer(geoJSON().addTo(mapRef.current)); } + const timer = useRef(null); + const handleMapClickEvent = (latlng: LatLng) => { if (timer?.current !== null) { return; diff --git a/source/frontend/src/components/maps/leaflet/Markers/SingleMarker.tsx b/source/frontend/src/components/maps/leaflet/Markers/SingleMarker.tsx index 6cee7e02df..9950f06c63 100644 --- a/source/frontend/src/components/maps/leaflet/Markers/SingleMarker.tsx +++ b/source/frontend/src/components/maps/leaflet/Markers/SingleMarker.tsx @@ -1,5 +1,6 @@ -import { LatLngLiteral } from 'leaflet'; -import { Marker } from 'react-leaflet'; +import { LatLngLiteral, LeafletMouseEvent } from 'leaflet'; +import { useRef } from 'react'; +import { Marker, useMap } from 'react-leaflet'; import { PointFeature } from 'supercluster'; import { useMapStateMachine } from '@/components/common/mapFSM/MapStateMachineContext'; @@ -8,6 +9,7 @@ import { PIMS_Property_Boundary_View, PIMS_Property_Location_View, } from '@/models/layers/pimsPropertyLocationView'; +import { useTenant } from '@/tenants'; import { getMarkerIcon, @@ -34,6 +36,7 @@ const SinglePropertyMarker: React.FC { const mapMachine = useMapStateMachine(); + const map = useMap(); const getIcon = ( feature: PointFeature< @@ -53,6 +56,24 @@ const SinglePropertyMarker: React.FC { + if (timer?.current !== null) { + return; + } + timer.current = setTimeout(() => { + onMarkerClicked(); + timer.current = null; + }, doubleClickInterval ?? 250); + }; + + const handleDoubleClickEvent = () => { + clearTimeout(timer?.current); + timer.current = null; + }; + const onMarkerClicked = () => { const clusterId = pointFeature.id?.toString() || 'ERROR_NO_ID'; const [longitude, latitude] = pointFeature.geometry.coordinates; @@ -95,8 +116,12 @@ const SinglePropertyMarker: React.FC { + map.fireEvent('dblclick', event); // bubble up double click events to the map. + handleDoubleClickEvent(); + }, click: () => { - onMarkerClicked(); + handleMarkerClickEvent(); }, }} /> diff --git a/source/frontend/src/features/mapSideBar/acquisition/__snapshots__/AcquisitionView.test.tsx.snap b/source/frontend/src/features/mapSideBar/acquisition/__snapshots__/AcquisitionView.test.tsx.snap index 66e4083176..deb88873d1 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/__snapshots__/AcquisitionView.test.tsx.snap +++ b/source/frontend/src/features/mapSideBar/acquisition/__snapshots__/AcquisitionView.test.tsx.snap @@ -1799,7 +1799,9 @@ exports[`AcquisitionView component > renders as expected 1`] = ` rel="noopener noreferrer" target="_blank" > - + + Luke Skywalker + { expect(getByTestId('assigned-date')).toHaveTextContent('Dec 18, 2024'); }); + it('renders multiple owner solicitor information with primary contact', async () => { + const { findByText, findAllByText } = setup( + { + acquisitionFile: { + ...mockAcquisitionFileResponse(), + acquisitionFileInterestHolders: [ + { + interestHolderId: 1, + interestHolderType: toTypeCodeNullable(InterestHolderType.OWNER_SOLICITOR), + + acquisitionFileId: 1, + personId: null, + person: null, + organizationId: 1, + organization: { + ...getEmptyOrganization(), + id: 1, + name: 'Millennium Inc', + alias: 'M Inc', + incorporationNumber: '1234', + comment: '', + contactMethods: null, + isDisabled: false, + organizationAddresses: null, + organizationPersons: null, + rowVersion: null, + }, + interestHolderProperties: [], + primaryContactId: 1, + primaryContact: null, + comment: null, + isDisabled: false, + ...getEmptyBaseAudit(), + }, + { + interestHolderId: 2, + interestHolderType: toTypeCodeNullable(InterestHolderType.OWNER_SOLICITOR), + + acquisitionFileId: 1, + personId: null, + person: null, + organizationId: 2, + organization: { + ...getEmptyOrganization(), + id: 2, + name: 'Test Org', + alias: 'M Inc', + incorporationNumber: '12345', + comment: '', + contactMethods: null, + isDisabled: false, + organizationAddresses: null, + organizationPersons: null, + rowVersion: null, + }, + interestHolderProperties: [], + primaryContactId: 2, + primaryContact: null, + comment: null, + isDisabled: false, + ...getEmptyBaseAudit(), + }, + ], + }, + }, + { claims: [] }, + ); + await waitForEffects(); + expect(await findByText('Millennium Inc')).toBeVisible(); + expect(await findByText('Test Org')).toBeVisible(); + expect(await findAllByText(/Primary contact/)).toHaveLength(2); + }); + it('renders owner solicitor information with primary contact', async () => { const { findByText } = setup( { acquisitionFile: mockAcquisitionFileResponse() }, { claims: [] }, ); + await waitForEffects(); expect(await findByText('Millennium Inc')).toBeVisible(); expect(await findByText(/Primary contact/)).toBeVisible(); expect(await findByText('Foo Bar Baz')).toBeVisible(); diff --git a/source/frontend/src/features/mapSideBar/acquisition/tabs/fileDetails/detail/AcquisitionSummaryView.tsx b/source/frontend/src/features/mapSideBar/acquisition/tabs/fileDetails/detail/AcquisitionSummaryView.tsx index facc83032d..1f4d2d8f33 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/tabs/fileDetails/detail/AcquisitionSummaryView.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/tabs/fileDetails/detail/AcquisitionSummaryView.tsx @@ -1,5 +1,5 @@ import Multiselect from 'multiselect-react-dropdown'; -import React, { useEffect } from 'react'; +import React from 'react'; import { FaExternalLinkAlt } from 'react-icons/fa'; import { Link } from 'react-router-dom'; import styled from 'styled-components'; @@ -15,6 +15,7 @@ import { Claims, Roles } from '@/constants'; import { InterestHolderType } from '@/constants/interestHolderTypes'; import { usePersonRepository } from '@/features/contacts/repositories/usePersonRepository'; import useKeycloakWrapper from '@/hooks/useKeycloakWrapper'; +import useDeepCompareEffect from '@/hooks/util/useDeepCompareEffect'; import { ApiGen_Base_CodeType } from '@/models/api/generated/ApiGen_Base_CodeType'; import { ApiGen_Concepts_AcquisitionFile } from '@/models/api/generated/ApiGen_Concepts_AcquisitionFile'; import { exists, prettyFormatDate } from '@/utils'; @@ -39,6 +40,10 @@ const AcquisitionSummaryView: React.FC = ({ const detail: DetailAcquisitionFile = DetailAcquisitionFile.fromApi(acquisitionFile); const { hasRole, hasClaim } = useKeycloakWrapper(); + const { + getPersonDetail: { execute: fetchPerson }, + } = usePersonRepository(); + const projectName = exists(acquisitionFile?.project) ? formatMinistryProject(acquisitionFile?.project?.code, acquisitionFile?.project?.description) : ''; @@ -47,13 +52,20 @@ const AcquisitionSummaryView: React.FC = ({ ? acquisitionFile?.product?.code + ' ' + acquisitionFile?.product?.description : ''; - const ownerSolicitor = acquisitionFile?.acquisitionFileInterestHolders?.find( + const ownerSolicitors = acquisitionFile?.acquisitionFileInterestHolders?.filter( x => x.interestHolderType?.id === InterestHolderType.OWNER_SOLICITOR, ); - const { - getPersonDetail: { execute: fetchPerson, response: ownerSolicitorPrimaryContact }, - } = usePersonRepository(); + useDeepCompareEffect(() => { + const getSolicitorPrimaryContacts = async () => { + ownerSolicitors + ?.filter(os => exists(os?.primaryContactId)) + .map(async os => { + os.primaryContact = await fetchPerson(os.primaryContactId); + }); + }; + getSolicitorPrimaryContacts(); + }, [ownerSolicitors, fetchPerson]); const selectedProgressStatuses: ApiGen_Base_CodeType[] = acquisitionFile?.acquisitionFileProgressStatuses @@ -65,13 +77,7 @@ const AcquisitionSummaryView: React.FC = ({ .map(x => x.takingStatusTypeCode) .filter(exists) ?? []; - useEffect(() => { - if (ownerSolicitor?.primaryContactId) { - fetchPerson(ownerSolicitor?.primaryContactId); - } - }, [ownerSolicitor?.primaryContactId, fetchPerson]); - - const ownerRepresentative = acquisitionFile?.acquisitionFileInterestHolders?.find( + const ownerRepresentatives = acquisitionFile?.acquisitionFileInterestHolders?.filter( x => x.interestHolderType?.id === InterestHolderType.OWNER_REPRESENTATIVE, ); @@ -244,59 +250,64 @@ const AcquisitionSummaryView: React.FC = ({ View={AcquisitionOwnersSummaryView} > )} - {!!ownerSolicitor && ( - - - - {ownerSolicitor?.personId - ? formatApiPersonNames(ownerSolicitor?.person) - : ownerSolicitor?.organization?.name ?? ''} - - - - - )} - {ownerSolicitor?.organization && ( - - {ownerSolicitor?.primaryContactId ? ( - - {formatApiPersonNames(ownerSolicitorPrimaryContact)} - - - ) : ( - 'No contacts available' - )} - - )} - {!!ownerRepresentative && ( - <> - - ( + + + + + {ownerSolicitor?.personId + ? formatApiPersonNames(ownerSolicitor?.person) + : ownerSolicitor?.organization?.name ?? ''} + + + + + + {ownerSolicitor?.organization && ( + + {ownerSolicitor?.primaryContactId ? ( + + {formatApiPersonNames(ownerSolicitor.primaryContact)} + + + ) : ( + 'No contacts available' + )} + + )} + + ))} + {!!ownerRepresentatives?.length && + ownerRepresentatives.map(ownerRepresentative => ( + + - {formatApiPersonNames(ownerRepresentative?.person ?? undefined)} - - - - {ownerRepresentative?.comment} - - )} + + {formatApiPersonNames(ownerRepresentative?.person ?? undefined)} + + + + {ownerRepresentative?.comment} + + ))} ); diff --git a/source/frontend/src/features/mapSideBar/acquisition/tabs/fileDetails/update/UpdateAcquisitionForm.test.tsx b/source/frontend/src/features/mapSideBar/acquisition/tabs/fileDetails/update/UpdateAcquisitionForm.test.tsx index 198bd1b0f9..c35f145d33 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/tabs/fileDetails/update/UpdateAcquisitionForm.test.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/tabs/fileDetails/update/UpdateAcquisitionForm.test.tsx @@ -12,6 +12,7 @@ import { act, fakeText, fireEvent, + getByTitle, render, RenderOptions, screen, @@ -23,6 +24,12 @@ import { import { UpdateAcquisitionSummaryFormModel } from './models'; import { UpdateAcquisitionFileYupSchema } from './UpdateAcquisitionFileYupSchema'; import UpdateAcquisitionForm, { IUpdateAcquisitionFormProps } from './UpdateAcquisitionForm'; +import { toTypeCodeNullable } from '@/utils/formUtils'; +import { InterestHolderType } from '@/constants/interestHolderTypes'; +import { getEmptyOrganization } from '@/mocks/organization.mock'; +import { getEmptyPerson } from '@/mocks/contacts.mock'; +import { getEmptyBaseAudit } from '@/models/defaultInitializers'; +import { InterestHolderForm } from '../../stakeholders/update/models'; const mockAxios = new MockAdapter(axios); @@ -352,5 +359,83 @@ describe('UpdateAcquisitionForm component', () => { expect(getByText(/Sub-interest solicitor/i)).toBeVisible(); expect(getByText(/Sub-interest representative/i)).toBeVisible(); }); + + it('renders multiple solicitors if present', async () => { + const { getByTitle, getAllByText } = setup({ + initialValues: { + ...initialValues, + toApi: vi.fn(), + ownerSolicitors: [ + InterestHolderForm.fromApi( + { + interestHolderId: 1, + interestHolderType: toTypeCodeNullable(InterestHolderType.OWNER_SOLICITOR), + + acquisitionFileId: 1, + personId: null, + person: null, + organizationId: 1, + organization: { + ...getEmptyOrganization(), + id: 1, + name: 'Millennium Inc', + alias: 'M Inc', + incorporationNumber: '1234', + comment: '', + contactMethods: null, + isDisabled: false, + organizationAddresses: null, + organizationPersons: null, + rowVersion: null, + }, + interestHolderProperties: [], + primaryContactId: 1, + primaryContact: null, + comment: null, + isDisabled: false, + ...getEmptyBaseAudit(), + }, + InterestHolderType.OWNER_SOLICITOR, + ), + InterestHolderForm.fromApi( + { + interestHolderId: 2, + interestHolderType: toTypeCodeNullable(InterestHolderType.OWNER_SOLICITOR), + + acquisitionFileId: 1, + personId: null, + person: null, + organizationId: 2, + organization: { + ...getEmptyOrganization(), + id: 2, + name: 'Test Org', + alias: 'M Inc', + incorporationNumber: '12345', + comment: '', + contactMethods: null, + isDisabled: false, + organizationAddresses: null, + organizationPersons: null, + rowVersion: null, + }, + interestHolderProperties: [], + primaryContactId: 1, + primaryContact: null, + comment: null, + isDisabled: false, + ...getEmptyBaseAudit(), + }, + InterestHolderType.OWNER_SOLICITOR, + ), + ], + }, + }); + await waitForEffects(); + + expect(getByTitle(/O1/i)).toBeVisible(); + expect(getByTitle(/O2/i)).toBeVisible(); + expect(getAllByText('Owner solicitor:')).toHaveLength(2); + }); }); }); diff --git a/source/frontend/src/features/mapSideBar/acquisition/tabs/fileDetails/update/UpdateAcquisitionForm.tsx b/source/frontend/src/features/mapSideBar/acquisition/tabs/fileDetails/update/UpdateAcquisitionForm.tsx index e31f086197..776bb76a7b 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/tabs/fileDetails/update/UpdateAcquisitionForm.tsx +++ b/source/frontend/src/features/mapSideBar/acquisition/tabs/fileDetails/update/UpdateAcquisitionForm.tsx @@ -36,6 +36,7 @@ import { ProgressStatusModel } from '../../../models/ProgressStatusModel'; import { TakingTypeStatusModel } from '../../../models/TakingTypeStatusModel'; import { UpdateAcquisitionSummaryFormModel } from './models'; import StatusToolTip from './StatusToolTip'; +import { UpdateSolicitorsSubForm } from './UpdateSolicitorsSubForm'; export interface IUpdateAcquisitionFormProps { formikRef: React.Ref>; @@ -85,7 +86,6 @@ const AcquisitionDetailSubForm: React.FC<{ const acquisitionPhysFileTypes = getOptionsByType(API.ACQUISITION_PHYSICAL_FILE_STATUS_TYPES); const fileStatusTypeCodes = getOptionsByType(API.ACQUISITION_FILE_STATUS_TYPES); const acquisitionFundingTypes = getOptionsByType(API.ACQUISITION_FUNDING_TYPES); - const ownerSolicitorContact = values.ownerSolicitor.contact; const subfileInterestTypes = getOptionsByType(API.SUBFILE_INTEREST_TYPES); const acquisitionProgressStatusTypesOptions = getByType( @@ -129,22 +129,15 @@ const AcquisitionDetailSubForm: React.FC<{ } = useOrganizationRepository(); React.useEffect(() => { - if (ownerSolicitorContact?.organizationId) { - fetchOrganization(ownerSolicitorContact?.organizationId); - } - }, [ownerSolicitorContact?.organizationId, fetchOrganization]); + values.ownerSolicitors + .filter(os => isValidId(+os?.organizationId)) + .map(async os => { + os.contact.organization = await fetchOrganization(+os.organizationId); + }); + }, [values.ownerSolicitors, fetchOrganization]); const orgPersons = organization?.organizationPersons; - React.useEffect(() => { - if (orgPersons?.length === 0) { - setFieldValue('ownerSolicitor.primaryContactId', null); - } - if (orgPersons?.length === 1) { - setFieldValue('ownerSolicitor.primaryContactId', orgPersons[0].personId); - } - }, [orgPersons, setFieldValue]); - const primaryContacts: SelectOption[] = orgPersons?.map((orgPerson: ApiGen_Concepts_PersonOrganization) => { return { @@ -397,40 +390,47 @@ const AcquisitionDetailSubForm: React.FC<{ )} - - - - {ownerSolicitorContact?.organizationId && !ownerSolicitorContact?.personId && ( - - {primaryContacts.length > 1 ? ( - - ) : primaryContacts.length === 1 ? ( - primaryContacts[0].label - ) : ( - 'No contacts available' + {/** backwards-compatibility with PAIMS etl data, which may have multiple ASOLCTR values in a single acq file. */} + {values?.ownerSolicitors?.length > 1 ? ( + + ) : ( + <> + + + + {values.ownerSolicitors[0]?.organizationId && !values.ownerSolicitors[0]?.personId && ( + + {primaryContacts.length > 1 ? ( + + ) : primaryContacts.length === 1 ? ( + primaryContacts[0].label + ) : ( + 'No contacts available' + )} + )} - + )} diff --git a/source/frontend/src/features/mapSideBar/acquisition/tabs/fileDetails/update/UpdateSolicitorsSubForm.tsx b/source/frontend/src/features/mapSideBar/acquisition/tabs/fileDetails/update/UpdateSolicitorsSubForm.tsx new file mode 100644 index 0000000000..ebb05087e3 --- /dev/null +++ b/source/frontend/src/features/mapSideBar/acquisition/tabs/fileDetails/update/UpdateSolicitorsSubForm.tsx @@ -0,0 +1,50 @@ +import { FieldArray, useFormikContext } from 'formik'; +import React from 'react'; + +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 { SectionField } from '@/components/common/Section/SectionField'; + +import { InterestHolderForm } from '../../stakeholders/update/models'; +import { UpdateAcquisitionSummaryFormModel } from './models'; + +/** This sub form is only used for etl data, where multiple AOSLCTR may exist. In that case, allow the user to interact with those items. */ +export const UpdateSolicitorsSubForm: React.FunctionComponent = () => { + const { values } = useFormikContext(); + + return ( + ( + <> + {values.ownerSolicitors.map((ownerSolicitor: InterestHolderForm, index: number) => ( + + + + + + {ownerSolicitor.contact?.organizationId && !ownerSolicitor.contact?.personId && ( + + + + )} + + ))} + + )} + /> + ); +}; diff --git a/source/frontend/src/features/mapSideBar/acquisition/tabs/fileDetails/update/__snapshots__/UpdateAcquisitionForm.test.tsx.snap b/source/frontend/src/features/mapSideBar/acquisition/tabs/fileDetails/update/__snapshots__/UpdateAcquisitionForm.test.tsx.snap index 94f7307322..6208bb39f7 100644 --- a/source/frontend/src/features/mapSideBar/acquisition/tabs/fileDetails/update/__snapshots__/UpdateAcquisitionForm.test.tsx.snap +++ b/source/frontend/src/features/mapSideBar/acquisition/tabs/fileDetails/update/__snapshots__/UpdateAcquisitionForm.test.tsx.snap @@ -3961,8 +3961,8 @@ exports[`UpdateAcquisitionForm component > Sub-interest files > renders as expec > Sub-interest files > renders as expec > Sub-interest files > renders as expec >