Skip to content

Commit cedb4ef

Browse files
feat(#2159): Added new fields to the advanced search (#2318)
* feat(#2159): Added new fields to the advanced search
1 parent de6fdb1 commit cedb4ef

11 files changed

Lines changed: 220 additions & 39 deletions

File tree

backend/src/main/java/ca/bc/gov/app/controller/client/ClientController.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -318,9 +318,9 @@ public Flux<ClientListDto> advancedSearch(
318318
if (criteria == null) {
319319
criteria =
320320
new ClientAdvancedSearchCriteriaDto(
321-
null, null, null, null, null,
322-
null, null, null, null, null,
323-
null, null);
321+
null, null, null, null, null, null, null, null,
322+
null, null, null, null, null, null, null, null
323+
);
324324
}
325325

326326
log.info(

backend/src/main/java/ca/bc/gov/app/dto/ClientAdvancedSearchCriteriaDto.java

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,27 @@
1010
* Data transfer object that encapsulates the optional search criteria used by
1111
* the advanced client search endpoint.
1212
*
13-
* <p>All fields are optional when a field is {@code null} or blank it is
13+
* <p>All fields are optional - when a field is {@code null} or blank it is
1414
* ignored by the search query. Non-blank fields are combined with AND logic
1515
* to narrow down the result set.
1616
*
1717
* @param clientName the client's last name or organization name to search for
1818
* @param firstName the client's first name to search for
1919
* @param middleName the client's middle name to search for
20-
* @param clientStatus the client status code (e.g. {@code "ACT"}, {@code "DAC"})
21-
* @param clientType the client type code (e.g. {@code "I"} for individual,
20+
* @param clientStatus the client status code (for example {@code "ACT"} or {@code "DAC"})
21+
* @param clientType the client type code (for example {@code "I"} for individual or
2222
* {@code "C"} for corporation)
2323
* @param clientIdType the type of the client identification document
24-
* @param clientIdentification the client identification number
24+
* @param clientIdentification the client identification or registration value to match
2525
* @param emailAddress the email address associated with the client contact or location
2626
* @param contactName the name of a contact person associated with the client
27-
* @param userId the user ID performing the search
28-
* @param updatedFromDate the lower bound for the last updated date (inclusive)
29-
* @param updatedToDate the upper bound for the last updated date (inclusive)
27+
* @param userId the updating user ID to search for
28+
* @param updatedFromDate the lower bound for the last updated date, inclusive
29+
* @param updatedToDate the upper bound for the last updated date, inclusive
30+
* @param birthdate the client's birthdate for exact-date matching
31+
* @param city the primary client location city to search for
32+
* @param postalCode the primary client location postal code to search for
33+
* @param comment text to match against client or primary location comments
3034
*/
3135
@JsonInclude(JsonInclude.Include.NON_NULL)
3236
public record ClientAdvancedSearchCriteriaDto(
@@ -41,7 +45,11 @@ public record ClientAdvancedSearchCriteriaDto(
4145
String contactName,
4246
String userId,
4347
@JsonFormat(pattern = "yyyy-MM-dd") LocalDate updatedFromDate,
44-
@JsonFormat(pattern = "yyyy-MM-dd") LocalDate updatedToDate
48+
@JsonFormat(pattern = "yyyy-MM-dd") LocalDate updatedToDate,
49+
@JsonFormat(pattern = "yyyy-MM-dd") LocalDate birthdate,
50+
String city,
51+
String postalCode,
52+
String comment
4553
) {
4654

4755
/**
@@ -69,7 +77,11 @@ public boolean hasValidParams() {
6977
sanitizedCriteria.contactName,
7078
sanitizedCriteria.userId,
7179
sanitizedCriteria.updatedFromDate,
72-
sanitizedCriteria.updatedToDate)
80+
sanitizedCriteria.updatedToDate,
81+
sanitizedCriteria.birthdate,
82+
sanitizedCriteria.city,
83+
sanitizedCriteria.postalCode,
84+
sanitizedCriteria.comment)
7385
.anyMatch(java.util.Objects::nonNull);
7486
}
7587

@@ -90,7 +102,11 @@ public ClientAdvancedSearchCriteriaDto sanitized() {
90102
blankToNull(contactName),
91103
blankToNull(userId),
92104
blankToNull(updatedFromDate),
93-
blankToNull(updatedToDate)
105+
blankToNull(updatedToDate),
106+
blankToNull(birthdate),
107+
blankToNull(city),
108+
blankToNull(postalCode),
109+
blankToNull(comment)
94110
);
95111
}
96112

backend/src/test/java/ca/bc/gov/app/controller/client/ClientControllerIntegrationTest.java

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,7 @@
1111
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
1212
import static org.junit.jupiter.api.Named.named;
1313
import static org.junit.jupiter.params.provider.Arguments.argumentSet;
14-
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf;
15-
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockJwt;
16-
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockUser;
14+
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.*;
1715
import ca.bc.gov.app.ApplicationConstant;
1816
import ca.bc.gov.app.BcRegistryTestConstants;
1917
import ca.bc.gov.app.TestConstants;
@@ -555,7 +553,8 @@ void shouldSearchAutocompleteRelatedClients(
555553
Long expectedSize
556554
) {
557555

558-
String legacyResponse = expectedSize > 0 ? "[{\"clientNumber\":\"" + clientNumber + "\"}]" : "[]";
556+
String legacyResponse =
557+
expectedSize > 0 ? "[{\"clientNumber\":\"" + clientNumber + "\"}]" : "[]";
559558

560559
legacyStub
561560
.stubFor(
@@ -778,7 +777,8 @@ private static Stream<Arguments> related() {
778777
"Double location",
779778
"00000003",
780779
Map.of("00", 1L, "01", 1L),
781-
"[{\"relatedClntLocn\": \"00\",\"primaryClient\": false},{\"clientLocnCode\": \"01\",\"primaryClient\": true}]"
780+
"[{\"relatedClntLocn\": \"00\",\"primaryClient\": false},"
781+
+ "{\"clientLocnCode\": \"01\",\"primaryClient\": true}]"
782782
)
783783
);
784784
}
@@ -1094,6 +1094,10 @@ void shouldAdvancedSearchWithResults() {
10941094
null,
10951095
null,
10961096
null,
1097+
null,
1098+
null,
1099+
null,
1100+
null,
10971101
null
10981102
)
10991103
)
@@ -1152,6 +1156,10 @@ void shouldAdvancedSearchWithNoResults() {
11521156
null,
11531157
null,
11541158
null,
1159+
null,
1160+
null,
1161+
null,
1162+
null,
11551163
null
11561164
)
11571165
)
@@ -1219,6 +1227,10 @@ void shouldAdvancedSearchWithDefaultPagination() {
12191227
null,
12201228
null,
12211229
null,
1230+
null,
1231+
null,
1232+
null,
1233+
null,
12221234
null
12231235
)
12241236
)

frontend/src/dto/CommonTypesDto.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,10 @@ export interface ClientSearchParameters {
197197
emailAddress?: string;
198198
updatedFromDate?: string;
199199
updatedToDate?: string;
200+
birthdate?: string;
201+
city?: string;
202+
postalCode?: string;
203+
comment?: string;
200204
};
201205

202206
export interface ClientSearchResult {

frontend/src/pages/search/AdvancedSearch.vue

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,16 @@ const validations = reactive<{ [key in keyof ClientSearchParameters]: ((value: s
6868
isMaxSizeMsg("email", 100),
6969
hasOnlyEmailCharacters("email"),
7070
],
71+
birthdate: [],
72+
city: getNamingValidations("city"),
73+
postalCode: [
74+
isMaxSizeMsg("postal code", 10),
75+
isAscii("postal code"),
76+
],
77+
comment: [
78+
isMaxSizeMsg("comment", 4000),
79+
isAscii("comment"),
80+
],
7181
updatedFromDate: [],
7282
updatedToDate: [],
7383
});
@@ -132,6 +142,10 @@ const clearFilters = () => {
132142
clientIdentification: undefined,
133143
contactName: undefined,
134144
emailAddress: undefined,
145+
birthdate: undefined,
146+
city: undefined,
147+
postalCode: undefined,
148+
comment: undefined,
135149
updatedFromDate: undefined,
136150
updatedToDate: undefined,
137151
};
@@ -141,6 +155,7 @@ const clearFilters = () => {
141155
validationState[key] = true;
142156
});
143157
158+
validations.birthdate = [];
144159
validations.updatedFromDate = [];
145160
validations.updatedToDate = [];
146161
@@ -326,6 +341,58 @@ const clearFilters = () => {
326341
@error="validationState.emailAddress = !$event"
327342
/>
328343
</div>
344+
<div class="horizontal-input-grouping-2">
345+
<div class="grouping-03">
346+
<div class="cds-text-input-label">
347+
<span>Birthdate</span>
348+
</div>
349+
<date-input-component
350+
id="birthdate"
351+
title="Birthdate"
352+
:autocomplete="['off', 'off', 'off']"
353+
v-model="modalFilters.birthdate"
354+
:enabled="true"
355+
:validations="validations.birthdate"
356+
@error="validationState.birthdate = !$event"
357+
@possibly-valid="validationState.birthdate = $event"
358+
/>
359+
</div>
360+
<text-input-component
361+
id="comment"
362+
label="Notes"
363+
placeholder=""
364+
tip=""
365+
autocomplete="off"
366+
v-model="modalFilters.comment"
367+
:validations="validations.comment"
368+
@empty="validationState.comment = true"
369+
@error="validationState.comment = !$event"
370+
/>
371+
</div>
372+
<div class="horizontal-input-grouping-2">
373+
<text-input-component
374+
id="city"
375+
label="City"
376+
placeholder=""
377+
tip=""
378+
autocomplete="off"
379+
v-model="modalFilters.city"
380+
:validations="validations.city"
381+
@empty="validationState.city = true"
382+
@error="validationState.city = !$event"
383+
/>
384+
<text-input-component
385+
id="postalCode"
386+
label="Postal code"
387+
placeholder=""
388+
tip=""
389+
autocomplete="off"
390+
v-model="modalFilters.postalCode"
391+
:validations="validations.postalCode"
392+
@empty="validationState.postalCode = true"
393+
@error="validationState.postalCode = !$event"
394+
/>
395+
</div>
329396
<div class="horizontal-input-grouping-2">
330397
<div class="grouping-03">
331398
<div class="cds-text-input-label">

frontend/tests/components/pages/search/AdvancedSearch.cy.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,14 @@ describe("<advanced-search />", () => {
101101
cy.fillFormEntry("#contactName", "sample-contactName");
102102
cy.fillFormEntry("#emailAddress", "sample-emailAddress");
103103

104+
cy.fillFormEntry("#birthdateYear", "1990");
105+
cy.fillFormEntry("#birthdateMonth", "06");
106+
cy.fillFormEntry("#birthdateDay", "15");
107+
108+
cy.fillFormEntry("#city", "sample-city");
109+
cy.fillFormEntry("#postalCode", "V8X3K2");
110+
cy.fillFormEntry("#comment", "sample-comment");
111+
104112
cy.fillFormEntry("#updatedFromDateYear", "2025");
105113
cy.fillFormEntry("#updatedFromDateMonth", "04");
106114
cy.fillFormEntry("#updatedFromDateDay", "15");
@@ -120,6 +128,10 @@ describe("<advanced-search />", () => {
120128
clientStatus: ["SPN"],
121129
contactName: "sample-contactName",
122130
emailAddress: "sample-emailAddress",
131+
birthdate: "1990-06-15",
132+
city: "sample-city",
133+
postalCode: "V8X3K2",
134+
comment: "sample-comment",
123135
updatedFromDate: "2025-04-15",
124136
updatedToDate: "2026-04-15",
125137
});
@@ -166,4 +178,4 @@ describe("<advanced-search />", () => {
166178
expect(vueWrapper.emitted("search")).to.be.an("array").that.has.length(1);
167179
});
168180
});
169-
});
181+
});

legacy/src/main/java/ca/bc/gov/app/dto/ClientAdvancedSearchCriteriaDto.java

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,27 @@
88
* Data transfer object that encapsulates the optional search criteria used by
99
* the advanced client search endpoint.
1010
*
11-
* <p>All fields are optional when a field is {@code null} or blank it is
11+
* <p>All fields are optional - when a field is {@code null} or blank it is
1212
* ignored by the search query. Non-blank fields are combined with AND logic
1313
* to narrow down the result set.
1414
*
1515
* @param clientName the client's last name or organization name to search for
1616
* @param firstName the client's first name to search for
1717
* @param middleName the client's middle name to search for
18-
* @param clientStatus the client status code (e.g. {@code "ACT"}, {@code "DAC"})
19-
* @param clientType the client type code (e.g. {@code "I"} for individual,
18+
* @param clientStatus the client status code (for example {@code "ACT"} or {@code "DAC"})
19+
* @param clientType the client type code (for example {@code "I"} for individual or
2020
* {@code "C"} for corporation)
2121
* @param clientIdType the type of the client identification document
22-
* @param clientIdentification the client identification number
22+
* @param clientIdentification the client identification or registration value to match
2323
* @param emailAddress the email address associated with the client contact or location
2424
* @param contactName the name of a contact person associated with the client
25-
* @param userId the user ID performing the search
26-
* @param updatedFromDate the lower bound for the last updated date (inclusive)
27-
* @param updatedToDate the upper bound for the last updated date (inclusive)
25+
* @param userId the updating user ID to search for
26+
* @param updatedFromDate the lower bound for the last updated date, inclusive
27+
* @param updatedToDate the upper bound for the last updated date, inclusive
28+
* @param birthdate the client's birthdate for exact-date matching
29+
* @param city the primary client location city to search for
30+
* @param postalCode the primary client location postal code to search for
31+
* @param comment text to match against client or primary location comments
2832
*/
2933
public record ClientAdvancedSearchCriteriaDto(
3034
String clientName,
@@ -38,7 +42,11 @@ public record ClientAdvancedSearchCriteriaDto(
3842
String contactName,
3943
String userId,
4044
LocalDate updatedFromDate,
41-
LocalDate updatedToDate
45+
LocalDate updatedToDate,
46+
LocalDate birthdate,
47+
String city,
48+
String postalCode,
49+
String comment
4250
) {
4351

4452
/**
@@ -66,13 +74,20 @@ public boolean hasValidParams() {
6674
sanitizedCriteria.contactName,
6775
sanitizedCriteria.userId,
6876
sanitizedCriteria.updatedFromDate,
69-
sanitizedCriteria.updatedToDate)
77+
sanitizedCriteria.updatedToDate,
78+
sanitizedCriteria.birthdate,
79+
sanitizedCriteria.city,
80+
sanitizedCriteria.postalCode,
81+
sanitizedCriteria.comment)
7082
.anyMatch(java.util.Objects::nonNull);
7183
}
7284

7385
/**
74-
* Returns a new instance with all blank/empty values converted to null,
75-
* so the SQL query can skip them via {@code :param IS NULL}.
86+
* Returns a new instance with all blank or empty string values converted to
87+
* {@code null} so the SQL query can skip them via {@code :param IS NULL}.
88+
*
89+
* @return a normalized copy of this criteria with blank string values
90+
* replaced by {@code null}
7691
*/
7792
public ClientAdvancedSearchCriteriaDto sanitized() {
7893
return new ClientAdvancedSearchCriteriaDto(
@@ -87,23 +102,28 @@ public ClientAdvancedSearchCriteriaDto sanitized() {
87102
blankToNull(contactName),
88103
blankToNull(userId),
89104
blankToNull(updatedFromDate),
90-
blankToNull(updatedToDate)
105+
blankToNull(updatedToDate),
106+
blankToNull(birthdate),
107+
blankToNull(city),
108+
blankToNull(postalCode),
109+
blankToNull(comment)
91110
);
92111
}
93112

94113
/**
95114
* Returns {@code null} if the given value is a {@link String} that is blank;
96115
* otherwise returns the original value unchanged.
97116
*
98-
* <p>A value is considered blank if it is {@code null}, empty, or contains only
99-
* whitespace (as defined by {@code StringUtils.isBlank}).
117+
* <p>A value is considered blank if it is {@code null}, empty, or contains
118+
* only whitespace as defined by {@link StringUtils#isBlank(CharSequence)}.
100119
*
101-
* <p>For non-{@link String} types (e.g., {@link java.time.LocalDateTime}),
102-
* the value is returned as-is.
120+
* <p>For non-{@link String} types such as {@link java.time.LocalDate}, the
121+
* value is returned as-is.
103122
*
104123
* @param <T> the type of the input value
105124
* @param value the value to check
106-
* @return {@code null} if the value is a blank {@link String}; otherwise the original value
125+
* @return {@code null} if the value is a blank {@link String}; otherwise the
126+
* original value
107127
*/
108128
private static <T> T blankToNull(T value) {
109129
if (value instanceof String str) {

0 commit comments

Comments
 (0)