From 808ae4b518db28d5f24e3ded828f0978f5ee2cad Mon Sep 17 00:00:00 2001 From: Paulo Gomes da Cruz Junior Date: Tue, 13 Aug 2024 14:47:51 -0700 Subject: [PATCH 01/23] chore: adding new flag to match result and changing contact and location setting contact and location as fuzzy true to allow the frontend to deal with its visual, while adding new flag to represent the source of the result --- .../ca/bc/gov/app/dto/client/MatchResult.java | 3 +- .../client/matches/ContactStepMatcher.java | 133 +++++++++------- .../matches/FirstNationsStepMatcher.java | 6 +- .../client/matches/IndividualStepMatcher.java | 3 + .../client/matches/LocationStepMatcher.java | 145 ++++++++++-------- .../client/matches/OthersStepMatcher.java | 3 + .../client/matches/RegisteredStepMatcher.java | 7 + .../service/client/matches/StepMatcher.java | 5 +- ...atchLocationControllerIntegrationTest.java | 3 +- .../matches/ContactStepMatcherTest.java | 2 +- .../matches/LocationStepMatcherTest.java | 2 +- 11 files changed, 185 insertions(+), 127 deletions(-) diff --git a/backend/src/main/java/ca/bc/gov/app/dto/client/MatchResult.java b/backend/src/main/java/ca/bc/gov/app/dto/client/MatchResult.java index 2218493dd2..f3dc2731c5 100644 --- a/backend/src/main/java/ca/bc/gov/app/dto/client/MatchResult.java +++ b/backend/src/main/java/ca/bc/gov/app/dto/client/MatchResult.java @@ -3,6 +3,7 @@ public record MatchResult( String field, String match, - boolean fuzzy + boolean fuzzy, + boolean partialMatch ) { } diff --git a/backend/src/main/java/ca/bc/gov/app/service/client/matches/ContactStepMatcher.java b/backend/src/main/java/ca/bc/gov/app/service/client/matches/ContactStepMatcher.java index 0e1a16b4e3..904aad4c01 100644 --- a/backend/src/main/java/ca/bc/gov/app/service/client/matches/ContactStepMatcher.java +++ b/backend/src/main/java/ca/bc/gov/app/service/client/matches/ContactStepMatcher.java @@ -2,6 +2,7 @@ import ca.bc.gov.app.dto.client.ClientContactDto; import ca.bc.gov.app.dto.client.ClientSubmissionDto; +import ca.bc.gov.app.dto.client.MatchResult; import ca.bc.gov.app.dto.client.StepMatchEnum; import ca.bc.gov.app.dto.legacy.ContactSearchDto; import ca.bc.gov.app.exception.InvalidRequestObjectException; @@ -93,64 +94,82 @@ public Mono matchStep(ClientSubmissionDto dto) { .stream() // Fix nonexistent index .map(address -> address.withIndexed(indexCounter.getAndIncrement())) - .map(contact -> - //Concat all the results for each address - Flux.concat( - processResult( - legacyService - .searchGeneric( - "email", - contact.email() - ), - FIELD_NAME_PREFIX + contact.index() + "].emailAddress", - false - ).as(Flux::from), - processResult( - legacyService - .searchGeneric( - PHONE_CONSTANT, - contact.phoneNumber() - ), - FIELD_NAME_PREFIX + contact.index() + "].businessPhoneNumber", - false - ).as(Flux::from), - processResult( - legacyService - .searchGeneric( - PHONE_CONSTANT, - contact.secondaryPhoneNumber() - ), - FIELD_NAME_PREFIX + contact.index() + "].secondaryPhoneNumber", - false - ).as(Flux::from), - processResult( - legacyService - .searchGeneric( - PHONE_CONSTANT, - contact.faxNumber() - ), - FIELD_NAME_PREFIX + contact.index() + "].faxNumber", - false - ).as(Flux::from), - processResult( - legacyService - .searchContact( - new ContactSearchDto( - contact.firstName(), - null, - contact.lastName(), - contact.email(), - contact.phoneNumber(), - contact.secondaryPhoneNumber(), - contact.faxNumber() - ) - ), - FIELD_NAME_PREFIX + contact.index() + "].firstName", - true - ).as(Flux::from) - ) - ) + //Concat all the results for each address + .map(this::processSingleContact) .reduce(Flux.empty(), Flux::concat) .as(this::reduceMatchResults); } + + private Flux processSingleContact(ClientContactDto contact) { + + Mono contactEmailFull = processResult( + legacyService + .searchGeneric( + "email", + contact.email() + ), + FIELD_NAME_PREFIX + contact.index() + "].email", + true, + false + ); + + Mono businessPhoneFull = processResult( + legacyService + .searchGeneric( + PHONE_CONSTANT, + contact.phoneNumber() + ), + FIELD_NAME_PREFIX + contact.index() + "].phoneNumber", + true, + false + ); + + Mono secondaryPhoneFull = processResult( + legacyService + .searchGeneric( + PHONE_CONSTANT, + contact.secondaryPhoneNumber() + ), + FIELD_NAME_PREFIX + contact.index() + "].secondaryPhoneNumber", + true, + false + ); + + Mono faxPhoneFull = processResult( + legacyService + .searchGeneric( + PHONE_CONSTANT, + contact.faxNumber() + ), + FIELD_NAME_PREFIX + contact.index() + "].faxNumber", + true, + false + ); + + Mono contactFull = processResult( + legacyService + .searchContact( + new ContactSearchDto( + contact.firstName(), + null, + contact.lastName(), + contact.email(), + contact.phoneNumber(), + contact.secondaryPhoneNumber(), + contact.faxNumber() + ) + ), + FIELD_NAME_PREFIX + contact.index() + "].firstName", + true, + true + ); + + return Flux.concat( + contactEmailFull, + businessPhoneFull, + secondaryPhoneFull, + faxPhoneFull, + contactFull + ); + } } diff --git a/backend/src/main/java/ca/bc/gov/app/service/client/matches/FirstNationsStepMatcher.java b/backend/src/main/java/ca/bc/gov/app/service/client/matches/FirstNationsStepMatcher.java index 7dc9151823..3af17911c2 100644 --- a/backend/src/main/java/ca/bc/gov/app/service/client/matches/FirstNationsStepMatcher.java +++ b/backend/src/main/java/ca/bc/gov/app/service/client/matches/FirstNationsStepMatcher.java @@ -58,8 +58,6 @@ public StepMatchEnum getStepMatcher() { @Override public Mono matchStep(ClientSubmissionDto dto) { - //TODO: appears to not being called - //TODO: Update the processor Flux clientRegistrationFullMatch = legacyService .searchLegacy( @@ -105,6 +103,7 @@ public Mono matchStep(ClientSubmissionDto dto) { processResult( clientRegistrationFullMatch, "businessInformation.businessName", + false, false ), @@ -112,6 +111,7 @@ public Mono matchStep(ClientSubmissionDto dto) { processResult( clientNameFuzzyMatch, "businessInformation.businessName", + true, true ), @@ -119,6 +119,7 @@ public Mono matchStep(ClientSubmissionDto dto) { processResult( clientNameFullMatch, "businessInformation.businessName", + false, false ), @@ -126,6 +127,7 @@ public Mono matchStep(ClientSubmissionDto dto) { processResult( clientAcronymFullMatch, "businessInformation.clientAcronym", + false, false ) ) diff --git a/backend/src/main/java/ca/bc/gov/app/service/client/matches/IndividualStepMatcher.java b/backend/src/main/java/ca/bc/gov/app/service/client/matches/IndividualStepMatcher.java index b6a1e91859..8d494b0b0e 100644 --- a/backend/src/main/java/ca/bc/gov/app/service/client/matches/IndividualStepMatcher.java +++ b/backend/src/main/java/ca/bc/gov/app/service/client/matches/IndividualStepMatcher.java @@ -106,16 +106,19 @@ public Mono matchStep(ClientSubmissionDto dto) { processResult( individualFuzzyMatch, "businessInformation.businessName", + true, true ), processResult( individualFullMatch, "businessInformation.businessName", + false, false ), processResult( documentFullMatch, "businessInformation.identification", + false, false ) ) diff --git a/backend/src/main/java/ca/bc/gov/app/service/client/matches/LocationStepMatcher.java b/backend/src/main/java/ca/bc/gov/app/service/client/matches/LocationStepMatcher.java index e974275a45..229f78f5a8 100644 --- a/backend/src/main/java/ca/bc/gov/app/service/client/matches/LocationStepMatcher.java +++ b/backend/src/main/java/ca/bc/gov/app/service/client/matches/LocationStepMatcher.java @@ -1,7 +1,9 @@ package ca.bc.gov.app.service.client.matches; import ca.bc.gov.app.dto.client.ClientAddressDto; +import ca.bc.gov.app.dto.client.ClientLocationDto; import ca.bc.gov.app.dto.client.ClientSubmissionDto; +import ca.bc.gov.app.dto.client.MatchResult; import ca.bc.gov.app.dto.client.StepMatchEnum; import ca.bc.gov.app.dto.legacy.AddressSearchDto; import ca.bc.gov.app.exception.InvalidRequestObjectException; @@ -72,12 +74,12 @@ public Mono matchStep(ClientSubmissionDto dto) { // Check if any of the addresses are invalid if ( BooleanUtils.isFalse( - dto - .location() - .addresses() - .stream() - .map(ClientAddressDto::isValid) - .reduce(true, Boolean::logicalAnd) + dto + .location() + .addresses() + .stream() + .map(ClientAddressDto::isValid) + .reduce(true, Boolean::logicalAnd) ) ) { return Mono.error(new InvalidRequestObjectException("Invalid address information")); @@ -94,62 +96,83 @@ public Mono matchStep(ClientSubmissionDto dto) { .stream() // Fix nonexistent index .map(address -> address.withIndexed(indexCounter.getAndIncrement())) - .map(address -> - //Concat all the results for each address - Flux.concat( - processResult( - legacyService - .searchGeneric( - "email", - address.emailAddress() - ), - FIELD_NAME_PREFIX + address.index() + "].emailAddress", - false - ).as(Flux::from), - processResult( - legacyService - .searchGeneric( - PHONE_CONSTANT, - address.businessPhoneNumber() - ), - FIELD_NAME_PREFIX + address.index() + "].businessPhoneNumber", - false - ).as(Flux::from), - processResult( - legacyService - .searchGeneric( - PHONE_CONSTANT, - address.secondaryPhoneNumber() - ), - FIELD_NAME_PREFIX + address.index() + "].secondaryPhoneNumber", - false - ).as(Flux::from), - processResult( - legacyService - .searchGeneric( - PHONE_CONSTANT, - address.faxNumber() - ), - FIELD_NAME_PREFIX + address.index() + "].faxNumber", - false - ).as(Flux::from), - processResult( - legacyService - .searchLocation( - new AddressSearchDto( - address.streetAddress(), - address.city(), - address.province().value(), - address.postalCode(), - address.country().value() - ) - ), - FIELD_NAME_PREFIX + address.index() + "].streetAddress", - false - ).as(Flux::from) - ) - ) + //Concat all the results for each location + .map(this::processSingleLocation) .reduce(Flux.empty(), Flux::concat) .as(this::reduceMatchResults); } + + private Flux processSingleLocation(ClientAddressDto location) { + + Mono contactEmailFull = processResult( + legacyService + .searchGeneric( + "email", + location.emailAddress() + ), + FIELD_NAME_PREFIX + location.index() + "].emailAddress", + true, + false + ); + + Mono businessPhoneFull = processResult( + legacyService + .searchGeneric( + PHONE_CONSTANT, + location.businessPhoneNumber() + ), + FIELD_NAME_PREFIX + location.index() + "].businessPhoneNumber", + true, + false + ); + + Mono secondaryPhoneFull = processResult( + legacyService + .searchGeneric( + PHONE_CONSTANT, + location.secondaryPhoneNumber() + ), + FIELD_NAME_PREFIX + location.index() + "].secondaryPhoneNumber", + true, + false + ); + + Mono faxPhoneFull = processResult( + legacyService + .searchGeneric( + PHONE_CONSTANT, + location.faxNumber() + ), + FIELD_NAME_PREFIX + location.index() + "].faxNumber", + true, + false + ); + + Mono locationFull = processResult( + legacyService + .searchLocation( + new AddressSearchDto( + location.streetAddress(), + location.city(), + location.province().value(), + location.postalCode(), + location.country().value() + ) + ), + FIELD_NAME_PREFIX + location.index() + "].streetAddress", + true, + false + ); + + return Flux.concat( + contactEmailFull, + businessPhoneFull, + secondaryPhoneFull, + faxPhoneFull, + locationFull + ); + + + } + } diff --git a/backend/src/main/java/ca/bc/gov/app/service/client/matches/OthersStepMatcher.java b/backend/src/main/java/ca/bc/gov/app/service/client/matches/OthersStepMatcher.java index 0c4dd88cce..fd6e1fab84 100644 --- a/backend/src/main/java/ca/bc/gov/app/service/client/matches/OthersStepMatcher.java +++ b/backend/src/main/java/ca/bc/gov/app/service/client/matches/OthersStepMatcher.java @@ -92,6 +92,7 @@ public Mono matchStep(ClientSubmissionDto dto) { processResult( clientNameFuzzyMatch, "businessInformation.businessName", + true, true ), @@ -99,6 +100,7 @@ public Mono matchStep(ClientSubmissionDto dto) { processResult( clientNameFullMatch, "businessInformation.businessName", + false, false ), @@ -106,6 +108,7 @@ public Mono matchStep(ClientSubmissionDto dto) { processResult( clientAcronymFullMatch, "businessInformation.clientAcronym", + false, false ) ) diff --git a/backend/src/main/java/ca/bc/gov/app/service/client/matches/RegisteredStepMatcher.java b/backend/src/main/java/ca/bc/gov/app/service/client/matches/RegisteredStepMatcher.java index b2cffcc4d0..dbc73f4890 100644 --- a/backend/src/main/java/ca/bc/gov/app/service/client/matches/RegisteredStepMatcher.java +++ b/backend/src/main/java/ca/bc/gov/app/service/client/matches/RegisteredStepMatcher.java @@ -151,6 +151,7 @@ public Mono matchStep(ClientSubmissionDto dto) { processResult( clientNameFuzzyMatch, BUSINESS_FIELD_NAME, + true, true ), @@ -158,6 +159,7 @@ public Mono matchStep(ClientSubmissionDto dto) { processResult( clientNameFullMatch, BUSINESS_FIELD_NAME, + false, false ), @@ -166,6 +168,7 @@ public Mono matchStep(ClientSubmissionDto dto) { processResult( clientRegistrationFullMatch, BUSINESS_FIELD_NAME, + false, false ), @@ -173,6 +176,7 @@ public Mono matchStep(ClientSubmissionDto dto) { processResult( dbaFuzzyMatch, "businessInformation.doingBusinessAs", + true, true ), @@ -180,6 +184,7 @@ public Mono matchStep(ClientSubmissionDto dto) { processResult( dbaFullMatch, "businessInformation.doingBusinessAs", + false, false ), @@ -187,6 +192,7 @@ public Mono matchStep(ClientSubmissionDto dto) { processResult( clientAcronymFullMatch, "businessInformation.clientAcronym", + false, false ), @@ -195,6 +201,7 @@ public Mono matchStep(ClientSubmissionDto dto) { processResult( individualFuzzyMatch, BUSINESS_FIELD_NAME, + true, true ) ) diff --git a/backend/src/main/java/ca/bc/gov/app/service/client/matches/StepMatcher.java b/backend/src/main/java/ca/bc/gov/app/service/client/matches/StepMatcher.java index 4565ba7ba9..41fcd8ed2f 100644 --- a/backend/src/main/java/ca/bc/gov/app/service/client/matches/StepMatcher.java +++ b/backend/src/main/java/ca/bc/gov/app/service/client/matches/StepMatcher.java @@ -53,7 +53,8 @@ public interface StepMatcher { default Mono processResult( Flux response, String fieldName, - boolean isFuzzy + boolean isFuzzy, + boolean isPartial ) { return response .map(ForestClientDto::clientNumber) @@ -63,7 +64,7 @@ default Mono processResult( clientNumbers -> getLogger().info("Matched client number(s) [{}] for field {}", clientNumbers, fieldName) ) - .map(clientNumbers -> new MatchResult(fieldName, clientNumbers, isFuzzy)); + .map(clientNumbers -> new MatchResult(fieldName, clientNumbers, isFuzzy, isPartial)); } /** diff --git a/backend/src/test/java/ca/bc/gov/app/controller/client/ClientMatchLocationControllerIntegrationTest.java b/backend/src/test/java/ca/bc/gov/app/controller/client/ClientMatchLocationControllerIntegrationTest.java index b265f970ec..daa758c7a9 100644 --- a/backend/src/test/java/ca/bc/gov/app/controller/client/ClientMatchLocationControllerIntegrationTest.java +++ b/backend/src/test/java/ca/bc/gov/app/controller/client/ClientMatchLocationControllerIntegrationTest.java @@ -130,8 +130,7 @@ void shouldRunMatch( .expectStatus() .isEqualTo(HttpStatus.CONFLICT) .expectBodyList(MatchResult.class) - .value(values -> Assertions.assertEquals( - fuzzy, + .value(values -> Assertions.assertTrue( values .stream() .reduce(false, (acc, m) -> acc || m.fuzzy(), (a, b) -> a || b) diff --git a/backend/src/test/java/ca/bc/gov/app/service/client/matches/ContactStepMatcherTest.java b/backend/src/test/java/ca/bc/gov/app/service/client/matches/ContactStepMatcherTest.java index db33a79115..1b33125f63 100644 --- a/backend/src/test/java/ca/bc/gov/app/service/client/matches/ContactStepMatcherTest.java +++ b/backend/src/test/java/ca/bc/gov/app/service/client/matches/ContactStepMatcherTest.java @@ -73,7 +73,7 @@ void shouldMatchStep( matchResult .stream() .map(m -> (MatchResult) m) - .anyMatch(m -> m.fuzzy() == fuzzy), + .anyMatch(MatchResult::fuzzy), "MatchResult with fuzzy value %s", fuzzy ) diff --git a/backend/src/test/java/ca/bc/gov/app/service/client/matches/LocationStepMatcherTest.java b/backend/src/test/java/ca/bc/gov/app/service/client/matches/LocationStepMatcherTest.java index 55360c9916..33b0fbd345 100644 --- a/backend/src/test/java/ca/bc/gov/app/service/client/matches/LocationStepMatcherTest.java +++ b/backend/src/test/java/ca/bc/gov/app/service/client/matches/LocationStepMatcherTest.java @@ -72,7 +72,7 @@ void shouldMatchStep( matchResult .stream() .map(m -> (MatchResult) m) - .anyMatch(m -> m.fuzzy() == fuzzy), + .anyMatch(MatchResult::fuzzy), "MatchResult with fuzzy value %s", fuzzy ) From 89e6a63c77fb1942b35cfce71900c09c31f0a599 Mon Sep 17 00:00:00 2001 From: Paulo Gomes da Cruz Junior Date: Wed, 14 Aug 2024 06:38:54 -0700 Subject: [PATCH 02/23] feat(FSADT1-1396): added fuzzy matching for registered business --- frontend/src/assets/styles/global.scss | 5 +- .../forms/AutoCompleteInputComponent.vue | 42 ++++- .../forms/DropdownInputComponent.vue | 47 ++++- .../forms/MultiselectInputComponent.vue | 44 ++++- .../forms/TextareaInputComponent.vue | 29 ++- ...uzzyMatchNotificationGroupingComponent.vue | 165 +++++++++++------- .../IndividualClientInformationWizardStep.vue | 3 +- frontend/stub/mappings/fuzzy_matches.json | 55 +++++- 8 files changed, 290 insertions(+), 100 deletions(-) diff --git a/frontend/src/assets/styles/global.scss b/frontend/src/assets/styles/global.scss index 690f01babf..c91987cdf3 100644 --- a/frontend/src/assets/styles/global.scss +++ b/frontend/src/assets/styles/global.scss @@ -1176,7 +1176,10 @@ cds-textarea::part(label), .cds-text-input-label { color: var(--light-theme-text-text-primary, #131315); } -cds-text-input.warning::part(input) { +cds-text-input.warning::part(input), +cds-combo-box.warning::part(input), +cds-textarea.warning::part(input), +cds-multi-select.warning::part(input) { outline: 2px solid var(--cds-support-warning,#ffc300) } diff --git a/frontend/src/components/forms/AutoCompleteInputComponent.vue b/frontend/src/components/forms/AutoCompleteInputComponent.vue index 672f47e265..f8032cad2a 100644 --- a/frontend/src/components/forms/AutoCompleteInputComponent.vue +++ b/frontend/src/components/forms/AutoCompleteInputComponent.vue @@ -8,7 +8,7 @@ import type { CDSComboBox } from "@carbon/web-components"; import { useEventBus } from "@vueuse/core"; // Types import type { BusinessSearchResult, CodeNameType } from "@/dto/CommonTypesDto"; -import { isEmpty } from "@/dto/CommonTypesDto"; +import { isEmpty, type ValidationMessageType } from "@/dto/CommonTypesDto"; //Define the input properties for this component const props = withDefaults(defineProps<{ @@ -44,6 +44,8 @@ const error = ref(props.errorMessage ?? ""); const revalidateBus = useEventBus("revalidate-bus"); +const warning = ref(false); + watch( () => props.errorMessage, () => setError(props.errorMessage), @@ -123,9 +125,27 @@ watch([inputValue], () => { emitValueChange(inputValue.value); }); -const setError = (errorMessage: string | undefined) => { - error.value = errorMessage; - emit("error", error.value); +/** + * Sets the error and emits an error event. + * @param errorObject - the error object or string + */ +const setError = (errorObject: string | ValidationMessageType | undefined) => { + const errorMessage = typeof errorObject === "object" ? errorObject.errorMsg : errorObject; + error.value = errorMessage || ""; + + warning.value = false; + if (typeof errorObject === "object") { + warning.value = errorObject.warning; + } + + /* + The error should be emitted whenever it is found, instead of watching and emitting only when it + changes. + Because the empty event is always emitted, even when it remains the same payload, and then we + rely on empty(false) to consider a value "valid". In turn we need to emit a new error event after + an empty one to allow subscribers to know in case the field still has the same error. + */ + emit('error', error.value); } //We call all the validations @@ -138,7 +158,10 @@ const validateInput = (newValue: string) => { if (errorMessage) return true; return false; }) - .shift() ?? props.errorMessage, + .reduce( + (acc, errorMessage) => acc || errorMessage, + props.errorMessage, + ) ); } }; @@ -221,6 +244,7 @@ const safeHelperText = computed(() => props.tip || " "); props.tip || " "); :label="placeholder" :value="inputValue" filterable - :invalid="error ? true : false" + :invalid="!warning && error ? true : false" :aria-invalid="ariaInvalidString" - :invalid-text="error" + :invalid-text="!warning && error" + :warn="warning" + :warn-text="warning && error" @cds-combo-box-selected="selectAutocompleteItem" v-on:input="onTyping" @focus="isFocused = true" @@ -246,7 +272,7 @@ const safeHelperText = computed(() => props.tip || " "); :data-focus="id" :data-scroll="id" :data-id="'input-' + id" - v-shadow="3" + v-shadow="4" > (props.errorMessage ?? ""); const revalidateBus = useEventBus("revalidate-bus"); +const warning = ref(false); + //We set it as a separated ref due to props not being updatable const selectedValue = ref(props.initialValue); // This is to make the input list contains the selected value to show when component render @@ -59,6 +61,30 @@ const emitValueChange = (newValue: string): void => { emit("empty", isEmpty(newValue)); }; + +/** + * Sets the error and emits an error event. + * @param errorObject - the error object or string + */ +const setError = (errorObject: string | ValidationMessageType | undefined) => { + const errorMessage = typeof errorObject === "object" ? errorObject.errorMsg : errorObject; + error.value = errorMessage || ""; + + warning.value = false; + if (typeof errorObject === "object") { + warning.value = errorObject.warning; + } + + /* + The error should be emitted whenever it is found, instead of watching and emitting only when it + changes. + Because the empty event is always emitted, even when it remains the same payload, and then we + rely on empty(false) to consider a value "valid". In turn we need to emit a new error event after + an empty one to allow subscribers to know in case the field still has the same error. + */ + emit('error', error.value); +} + /** * Performs all validations and returns the first error message. * If there's no error from the validations, returns props.errorMessage. @@ -75,14 +101,17 @@ const validatePurely = (newValue: string): string | undefined => { if (errorMessage) return true; return false; }) - .shift() ?? props.errorMessage + .reduce( + (acc, errorMessage) => acc || errorMessage, + props.errorMessage + ) ); } } const validateInput = (newValue: any) => { if (props.validations) { - error.value = validatePurely(newValue); + setError(validatePurely(newValue)); } }; @@ -133,10 +162,9 @@ watch([selectedValue], () => { watch(inputList, () => (selectedValue.value = props.initialValue)); //We watch for error changes to emit events -watch(error, () => emit("error", error.value)); watch( () => props.errorMessage, - () => (error.value = props.errorMessage) + () => setError(props.errorMessage) ); watch( () => props.initialValue, @@ -203,6 +231,7 @@ const safeHelperText = computed(() => props.tip || " "); :autocomplete="autocomplete" :title-text="label" :aria-label="label" + :class="warning ? 'warning' : ''" :clear-selection-label="`Clear ${label}`" :required="required" :data-required-label="requiredLabel" @@ -210,9 +239,11 @@ const safeHelperText = computed(() => props.tip || " "); :helper-text="safeHelperText" :label="placeholder" :value="selectedValue" - :invalid="error ? true : false" + :invalid="!warning && error ? true : false" :aria-invalid="ariaInvalidString" - :invalidText="error" + :invalid-text="!warning && error" + :warn="warning" + :warn-text="warning && error" @cds-combo-box-selected="selectItem" @focus="isFocused = true" @blur=" @@ -223,7 +254,7 @@ const safeHelperText = computed(() => props.tip || " "); " :data-focus="id" :data-scroll="id" - v-shadow="3" + v-shadow="4" > (props.errorMessage ?? ""); const revalidateBus = useEventBus("revalidate-bus"); +const warning = ref(false); + //We set it as a separated ref due to props not being updatable const selectedValue = ref(props.initialValue); // This is to make the input list contains the selected value to show when component render @@ -51,6 +53,29 @@ emit("empty", props.selectedValues ? props.selectedValues.length === 0 : true); //Controls the selected values const items = ref([]); +/** + * Sets the error and emits an error event. + * @param errorObject - the error object or string + */ +const setError = (errorObject: string | ValidationMessageType | undefined) => { + const errorMessage = typeof errorObject === "object" ? errorObject.errorMsg : errorObject; + error.value = errorMessage || ""; + + warning.value = false; + if (typeof errorObject === "object") { + warning.value = errorObject.warning; + } + + /* + The error should be emitted whenever it is found, instead of watching and emitting only when it + changes. + Because the empty event is always emitted, even when it remains the same payload, and then we + rely on empty(false) to consider a value "valid". In turn we need to emit a new error event after + an empty one to allow subscribers to know in case the field still has the same error. + */ + emit('error', error.value); +} + //We call all the validations const validateInput = (newValue: any) => { if (props.validations && props.validations.length > 0) { @@ -62,9 +87,12 @@ const validateInput = (newValue: any) => { props.validations .map((validation) => validation(value)) .filter(hasError) - .shift() ?? props.errorMessage; + .reduce( + (acc, errorMessage) => acc || errorMessage, + props.errorMessage + ); - error.value = validate(items.value); + setError(validate(items.value)); } }; @@ -108,7 +136,7 @@ watch([selectedValue], () => validateInput(selectedValue.value)); watch(error, () => emit("error", error.value)); watch( () => props.errorMessage, - () => (error.value = props.errorMessage) + () => setError(props.errorMessage) ); revalidateBus.on(() => validateInput(selectedValue.value)); @@ -162,14 +190,17 @@ watch( :id="id" :value="selectedValue" :label="selectedValue" + :class="warning ? 'warning' : ''" :title-text="label" :aria-label="label" :required="required" :data-required-label="requiredLabel" :helper-text="tip" - :invalid="error ? true : false" + :invalid="!warning && error ? true : false" :aria-invalid="ariaInvalidString" - :invalid-text="error" + :invalid-text="!warning && error" + :warn="warning" + :warn-text="warning && error" filterable @cds-multi-select-selected="selectItems" @focus="isFocused = true" @@ -181,6 +212,7 @@ watch( " :data-focus="id" :data-scroll="id" + v-shadow="4" > (props.errorMessage ?? ""); const revalidateBus = useEventBus("revalidate-bus"); +const warning = ref(false); + /** * Sets the error and emits an error event. - * @param errorMessage - the error message + * @param errorObject - the error object or string */ -const setError = (errorMessage: string | undefined) => { - error.value = errorMessage; +const setError = (errorObject: string | ValidationMessageType | undefined) => { + const errorMessage = typeof errorObject === "object" ? errorObject.errorMsg : errorObject; + error.value = errorMessage || ""; + + warning.value = false; + if (typeof errorObject === "object") { + warning.value = errorObject.warning; + } /* The error should be emitted whenever it is found, instead of watching and emitting only when it @@ -53,8 +61,8 @@ const setError = (errorMessage: string | undefined) => { rely on empty(false) to consider a value "valid". In turn we need to emit a new error event after an empty one to allow subscribers to know in case the field still has the same error. */ - emit("error", error.value); -}; + emit('error', error.value); +} watch( () => props.errorMessage, @@ -130,6 +138,7 @@ const ariaInvalidString = computed(() => (error.value ? "true" : "false")); v-if="enabled" :id="id" :rows="rows" + :class="warning ? 'warning' : ''" :enable-counter="enableCounter" :max-count="maxCount" :required="required" @@ -140,9 +149,11 @@ const ariaInvalidString = computed(() => (error.value ? "true" : "false")); :value="selectedValue" :helper-text="tip" :disabled="!enabled" - :invalid="error ? true : false" + :invalid="!warning && error ? true : false" :aria-invalid="ariaInvalidString" - :invalid-text="error" + :invalid-text="!warning && error" + :warn="warning" + :warn-text="warning && error" @focus="isFocused = true" @blur=" (event: any) => { @@ -154,7 +165,7 @@ const ariaInvalidString = computed(() => (error.value ? "true" : "false")); :data-focus="id" :data-scroll="id" :data-id="'input-' + id" - v-shadow="3" + v-shadow="4" > diff --git a/frontend/src/components/grouping/FuzzyMatchNotificationGroupingComponent.vue b/frontend/src/components/grouping/FuzzyMatchNotificationGroupingComponent.vue index 0149579c0b..4daa03c2b9 100644 --- a/frontend/src/components/grouping/FuzzyMatchNotificationGroupingComponent.vue +++ b/frontend/src/components/grouping/FuzzyMatchNotificationGroupingComponent.vue @@ -31,72 +31,81 @@ const fuzzyMatchedError = ref( }, ); -const handleFuzzyErrorMessage = (event: FuzzyMatcherEvent | undefined, _payload?: any) => { - if (event && event.matches.length > 0 && event.id === props.id) { - fuzzyMatchedError.value.show = true; - fuzzyMatchedError.value.fuzzy = true; - fuzzyMatchedError.value.matches = []; - for (const rawMatch of event.matches) { - const match: MiscFuzzyMatchResult = { result: rawMatch }; - if (!rawMatch.fuzzy) { - fuzzyMatchedError.value.fuzzy = false; - } - fuzzyMatchedError.value.matches.push(match); +const fieldNameToDescription : Record = { + "businessInformation.businessName": "client name", + "businessInformation.firstName": "name", + "businessInformation.lastName": "name", + "businessInformation.birthdate": "date of birth", + "businessInformation.clientIdentification": "identification type", + "businessInformation.clientTypeOfId": "identification type", + "businessInformation.clientIdNumber": "identification number", + "identificationType.text": "identification type", + "identificationProvince.text": "identification province", + "businessInformation.clientAcronym": "client acronym", + "businessInformation.doingBusinessAs": "doing business as", + "businessInformation.workSafeBcNumber": "WorkSafeBC number", +}; - const identificationTypeGroup = ["identificationType.text", "identificationProvince.text"]; - const clientIdentificationGroup = [ - "businessInformation.clientIdentification", - "businessInformation.clientTypeOfId", - "businessInformation.clientIdNumber", - ]; - - const warning = rawMatch.fuzzy; - const createErrorEvent = (fieldList: string[]) => - fieldList.map((fieldId) => ({ - fieldId, - errorMsg: warning ? "There's already a client with this name" : "Client already exists", - })); - const emitFieldErrors = (fieldList: string[]) => { - const errorEvent = createErrorEvent(fieldList); - errorBus.emit(errorEvent, { - skipNotification: true, - warning, - }); - }; - const label = (matchedFieldsText) => { - const prefix = warning ? "Partial matching on" : "Matching on"; - return `${prefix} ${matchedFieldsText}`; - } - if (rawMatch.field === "businessInformation.businessName") { - if (rawMatch.fuzzy) { - match.label = label("name and date of birth"); - emitFieldErrors([ - "businessInformation.firstName", - "businessInformation.lastName", - "businessInformation.birthdate", - ]); - } else { - match.label = label("name, date of birth and ID number"); - emitFieldErrors([ - "businessInformation.firstName", - "businessInformation.lastName", - "businessInformation.birthdate", - ...clientIdentificationGroup, - ]); - } - } - if (rawMatch.field === "businessInformation.identification") { - match.label = label("ID type and ID number"); - emitFieldErrors([...identificationTypeGroup, ...clientIdentificationGroup]); - } - } - } else { - fuzzyMatchedError.value.show = false; - fuzzyMatchedError.value.fuzzy = false; - fuzzyMatchedError.value.matches = []; - } +const fieldNameToNamingGroups : Record = { + "businessInformation.businessName": ["businessInformation.businessName"], + "businessInformation.firstName": [ + "businessInformation.firstName", + "businessInformation.lastName", + "businessInformation.birthdate" + ], + "businessInformation.lastName": [ + "businessInformation.firstName", + "businessInformation.lastName", + "businessInformation.birthdate", + "businessInformation.identificationType", + "businessInformation.clientTypeOfId", + "businessInformation.clientIdNumber", + "identificationType.text", + "identificationProvince.text", + "businessInformation.clientIdentification", + ], + "businessInformation.clientIdentification": [ + "businessInformation.identificationType", + "businessInformation.clientTypeOfId", + "businessInformation.clientIdNumber", + "identificationType.text", + "identificationProvince.text", + "businessInformation.clientIdentification", + ], + "businessInformation.doingBusinessAs": ["businessInformation.doingBusinessAs"], + "businessInformation.clientAcronym": ["businessInformation.clientAcronym"], }; +const fieldNameToLabel : Record = { + "businessInformation.firstName": "name and date of birth", + "businessInformation.lastName": "name, date of birth and ID number", + "businessInformation.clientIdentification": "ID type and ID number", +}; + +const createErrorEvent = (fieldList: string[], warning: boolean) => + fieldList.map((fieldId) => ({ + fieldId, + errorMsg: warning ? `There's already a client with this "${fieldNameToDescription[fieldId]}"` : "Client already exists", + })); + +const emitFieldErrors = (fieldList: string[], warning: boolean) => { + const errorEvent = createErrorEvent(fieldList, warning); + errorBus.emit(errorEvent, { + skipNotification: true, + warning, + }); +}; + +const label = (matchedFieldsText: string, warning: boolean) => { + + if(!matchedFieldsText) + return ''; + + const prefix = warning ? "Partial matching on" : "Matching on"; + return `${prefix} ${matchedFieldsText}`; +} + + const getListItemContent = ref((match: MiscFuzzyMatchResult) => { return match && match.result?.match ? renderListItem(match) : ""; }); @@ -160,8 +169,38 @@ const getUniqueClientNumbers = (matches: MiscFuzzyMatchResult[]) => { const uniqueClientNumbers = computed(() => getUniqueClientNumbers(fuzzyMatchedError.value.matches)); + +const handleFuzzyErrorMessage = (event: FuzzyMatcherEvent | undefined, _payload?: any) => { + if (event && event.matches.length > 0 && event.id === props.id) { + + fuzzyMatchedError.value.show = true; + fuzzyMatchedError.value.fuzzy = true; + fuzzyMatchedError.value.matches = []; + + for (const rawMatch of event.matches) { + + const match: MiscFuzzyMatchResult = { result: rawMatch,label: label(fieldNameToLabel[rawMatch.field], rawMatch.fuzzy) }; + if (!rawMatch.fuzzy) { + fuzzyMatchedError.value.fuzzy = false; + } + + fuzzyMatchedError.value.matches.push(match); + emitFieldErrors(fieldNameToNamingGroups[rawMatch.field], rawMatch.fuzzy); + } + } else { + fuzzyMatchedError.value.show = false; + fuzzyMatchedError.value.fuzzy = false; + fuzzyMatchedError.value.matches = []; + } +}; + // watch for fuzzy match error messages fuzzyBus.on(handleFuzzyErrorMessage); + +// just for debugs +errorBus.on((errors,params) => { + console.log("errors", errors,"params", params); +});