Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add fuzzy matching notification #1060

Merged
merged 2 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ declare module 'vue' {
DateInputPart: typeof import('./src/components/forms/DateInputComponent/DateInputPart.vue')['default']
DropdownInputComponent: typeof import('./src/components/forms/DropdownInputComponent.vue')['default']
ErrorNotificationGroupingComponent: typeof import('./src/components/grouping/ErrorNotificationGroupingComponent.vue')['default']
FuzzyMatchNotificationGroupingComponent: typeof import('./src/components/grouping/FuzzyMatchNotificationGroupingComponent.vue')['default']
LoadingOverlayComponent: typeof import('./src/components/LoadingOverlayComponent.vue')['default']
MainHeaderComponent: typeof import('./src/components/MainHeaderComponent.vue')['default']
MultiselectInputComponent: typeof import('./src/components/forms/MultiselectInputComponent.vue')['default']
Expand Down
9 changes: 9 additions & 0 deletions frontend/cypress/e2e/FormStaffPage.Submission.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,10 +174,13 @@ describe("Staff Form Submission", () => {
it("should be success message", () => {
fillIndividual();
clickNext();
cy.contains("h2", "Locations");
fillLocation();
clickNext();
cy.contains("h2", "Contacts");
fillContact();
clickNext();
cy.contains("h2", "Review");

cy.get("[data-test='wizard-submit-button']").click();
cy.get("h1").should("contain", "New client 00123456 has been created!");
Expand All @@ -186,10 +189,13 @@ describe("Staff Form Submission", () => {
it("should be timeout message", () => {
fillIndividual();
clickNext();
cy.contains("h2", "Locations");
fillLocation();
clickNext();
cy.contains("h2", "Contacts");
fillContact();
clickNext();
cy.contains("h2", "Review");

cy.fillFormEntry("cds-textarea","error",10,true);

Expand All @@ -209,10 +215,13 @@ describe("Staff Form Submission", () => {
clientIdentification: "AB345678",
});
clickNext();
cy.contains("h2", "Locations");
fillLocation();
clickNext();
cy.contains("h2", "Contacts");
fillContact();
clickNext();
cy.contains("h2", "Review");

cy.get("[data-test='wizard-submit-button']").click();
cy.get("cds-actionable-notification")
Expand Down
7 changes: 6 additions & 1 deletion frontend/cypress/e2e/FormStaffPage.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,7 @@ describe("Staff Form", () => {

describe("when all the required fields are filled in", () => {
beforeEach(() => {
cy.contains("h2", "Locations");
fillLocation();
});

Expand Down Expand Up @@ -457,6 +458,7 @@ describe("Staff Form", () => {

cy.get("[data-test='wizard-next-button']").click();

cy.contains("h2", "Locations");
fillLocation();

cy.get("[data-test='wizard-next-button']").click();
Expand All @@ -468,6 +470,7 @@ describe("Staff Form", () => {

describe("when all the required fields are filled in", () => {
beforeEach(() => {
cy.contains("h2", "Contacts");
fillContact();
});

Expand Down Expand Up @@ -532,9 +535,11 @@ describe("Staff Form", () => {
});
cy.get("[data-test='wizard-next-button']").click();

cy.contains("h2", "Locations");
fillLocation();
cy.get("[data-test='wizard-next-button']").click();

cy.contains("h2", "Contacts");
fillContact();
cy.get("[data-test='wizard-next-button']").click();
});
Expand All @@ -558,7 +563,7 @@ describe("Staff Form", () => {
cy.get("cds-textarea")
.shadow()
.find("textarea")
.type("A".repeat(4010));
.type("A".repeat(4010), {delay: 0});

cy.get("cds-textarea")
.shadow()
Expand Down
9 changes: 6 additions & 3 deletions frontend/src/assets/styles/global.scss
Original file line number Diff line number Diff line change
Expand Up @@ -1479,11 +1479,14 @@ Useful for scrolling to the *start* of an HTML element without having it covered
top: calc(-1 * var(--header-height));
}

.hide-when-less-than-two-children {
.hide-when-less-than-two-children:not(:has(:nth-child(2))) {
display: none;
}
.hide-when-less-than-two-children:has(:nth-child(2)) {
display: unset;

.errors-container {
display: flex;
flex-direction: column;
gap: 1rem;
}

.invisible {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
<script setup lang="ts">
import { computed, reactive, ref, watch } from "vue";
// Carbon
import "@carbon/web-components/es/components/notification/index";
// Composables
import { useEventBus } from "@vueuse/core";
// Types
import type { FuzzyMatchResult, FuzzyMatcherData, FuzzyMatcherEvent } from "@/dto/CommonTypesDto";
import { greenDomain } from "@/CoreConstants";
import { convertFieldNameToSentence } from "@/services/ForestClientService";

const props = defineProps<{
id: string;
error?: FuzzyMatcherData;
businessName: string;
}>();

const fuzzyBus = useEventBus<FuzzyMatcherEvent>("fuzzy-error-notification");

const fuzzyMatchedError = ref<FuzzyMatcherData>(
props.error ?? {
show: false,
fuzzy: false,
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 match of event.matches) {
if (!match.fuzzy) {
fuzzyMatchedError.value.fuzzy = false;
}
fuzzyMatchedError.value.matches.push(match);
}
} else {
fuzzyMatchedError.value.show = false;
fuzzyMatchedError.value.fuzzy = false;
fuzzyMatchedError.value.matches = [];
}
};

const getListItemContent = ref((match: FuzzyMatchResult) => {
return match && match.match ? renderListItem(match) : "";
});

const getLegacyUrl = (duplicatedClient, label) => {
const encodedClientNumber = encodeURIComponent(duplicatedClient.trim());
switch (label) {
case "contact":
return `https://${greenDomain}/int/client/client06ContactListAction.do?bean.clientNumber=${encodedClientNumber}`;
case "location":
return `https://${greenDomain}/int/client/client07LocationListAction.do?bean.clientNumber=${encodedClientNumber}`;
default:
return `https://${greenDomain}/int/client/client02MaintenanceAction.do?bean.clientNumber=${encodedClientNumber}`;
}
};

const renderListItem = (match: FuzzyMatchResult) => {
let finalLabel = "";
if (match.field === "contact" || match.field === "location") {
finalLabel = "Matching one or more " + match.field + "s";
} else {
finalLabel =
(match.fuzzy ? "Partial m" : "M") +
"atching on " +
convertFieldNameToSentence(match.field).toLowerCase();
}

finalLabel += " - Client number: ";

const clients = [...new Set<string>(match.match.split(","))];
finalLabel += clients
.map(
(clientNumber) =>
'<a target="_blank" href="' +
getLegacyUrl(clientNumber, match.field) +
'">' +
clientNumber +
"</a>",
)
.join(", ");

return finalLabel;
};

/**
* Gets unique client numbers across all the matches
* @param matches
*/
const getUniqueClientNumbers = (matches: FuzzyMatchResult[]) => {
const results: string[] = [];

matches.forEach((data) => {
results.push(...data.match.split(","));
});

return [...new Set(results)];
};

const uniqueClientNumbers = computed(() => getUniqueClientNumbers(fuzzyMatchedError.value.matches));

// watch for fuzzy match error messages
fuzzyBus.on(handleFuzzyErrorMessage);
</script>

<template>
<cds-actionable-notification
v-if="fuzzyMatchedError.show"
v-shadow="true"
low-contrast="true"
hide-close-button="true"
open="true"
:kind="fuzzyMatchedError.fuzzy ? 'warning' : 'error'"
:title="fuzzyMatchedError.fuzzy ? 'Possible matching records found' : 'Client already exists'"
>
<div>
<span class="body-compact-02">
<template v-if="fuzzyMatchedError.fuzzy">
{{ uniqueClientNumbers.length }} similar client
<span v-if="uniqueClientNumbers.length === 1">record was</span>
<span v-else>records were</span>
found. Review their information in the Client Management System to determine if you should
should create a new client:
</template>
<template v-else>
Looks like ”{{ businessName }}” has a client number. Review their information in the
Management System if necessary:
</template>
</span>
<ul>
<!-- eslint-disable-next-line vue/no-v-html -->
<li
v-for="match in fuzzyMatchedError.matches"
:key="match.field"
v-dompurify-html="getListItemContent(match)"
></li>
</ul>
<span v-if="!fuzzyMatchedError.fuzzy" class="body-compact-02">
You must inform the applicant of their number.
</span>
</div>
</cds-actionable-notification>
</template>

<style scoped>
cds-actionable-notification > div {
display: flex;
flex-direction: column;
gap: 1rem;
}
cds-actionable-notification > div > ul {
margin: 0;
padding: 0;
list-style-type: disc;
margin-left: 1.25rem;
}
</style>
17 changes: 17 additions & 0 deletions frontend/src/dto/CommonTypesDto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,23 @@ export interface ValidationMessageType {
originalValue?: string;
}

export interface FuzzyMatchResult {
field: string;
match: string;
fuzzy: boolean;
}

export interface FuzzyMatcherData {
show: boolean;
fuzzy: boolean;
matches: FuzzyMatchResult[];
}

export interface FuzzyMatcherEvent {
id: string;
matches: FuzzyMatchResult[];
}

export const isEmpty = (receivedValue: any): boolean => {
const value = isRef(receivedValue) ? receivedValue.value : receivedValue;
return value === undefined || value === null || value === "";
Expand Down
Loading
Loading