diff --git a/pkg/handlers/adminapi/rejected_office_users.go b/pkg/handlers/adminapi/rejected_office_users.go index f638dd3aedf..238cc968631 100644 --- a/pkg/handlers/adminapi/rejected_office_users.go +++ b/pkg/handlers/adminapi/rejected_office_users.go @@ -68,8 +68,50 @@ type IndexRejectedOfficeUsersHandler struct { var rejectedOfficeUserFilterConverters = map[string]func(string) func(*pop.Query){ "search": func(content string) func(*pop.Query) { return func(query *pop.Query) { - nameSearch := fmt.Sprintf("%%%s%%", content) - query.Where("roles.role_name ILIKE ? AND office_users.status = 'REJECTED' OR transportation_offices.name ILIKE ? AND office_users.status = 'REJECTED' OR office_users.email ILIKE ? AND office_users.status = 'REJECTED' OR office_users.first_name ILIKE ? AND office_users.status = 'REJECTED' OR office_users.last_name ILIKE ? AND office_users.status = 'REJECTED'", nameSearch, nameSearch, nameSearch, nameSearch, nameSearch) + firstSearch, lastSearch, emailSearch := fmt.Sprintf("%%%s%%", content), fmt.Sprintf("%%%s%%", content), fmt.Sprintf("%%%s%%", content) + query.Where("office_users.first_name ILIKE ? AND office_users.status = 'REJECTED' OR office_users.email ILIKE ? AND office_users.status = 'REJECTED' OR office_users.last_name ILIKE ? AND office_users.status = 'REJECTED'", firstSearch, lastSearch, emailSearch) + } + }, + "emails": func(content string) func(*pop.Query) { + return func(query *pop.Query) { + emailSearch := fmt.Sprintf("%%%s%%", content) + query.Where("office_users.email ILIKE ? AND office_users.status = 'REJECTED'", emailSearch) + } + }, + "firstName": func(content string) func(*pop.Query) { + return func(query *pop.Query) { + firstNameSearch := fmt.Sprintf("%%%s%%", content) + query.Where("office_users.first_name ILIKE ? AND office_users.status = 'REJECTED'", firstNameSearch) + } + }, + "lastName": func(content string) func(*pop.Query) { + return func(query *pop.Query) { + lastNameSearch := fmt.Sprintf("%%%s%%", content) + query.Where("office_users.last_name ILIKE ? AND office_users.status = 'REJECTED'", lastNameSearch) + } + }, + "offices": func(content string) func(*pop.Query) { + return func(query *pop.Query) { + officeSearch := fmt.Sprintf("%%%s%%", content) + query.Where("transportation_offices.name ILIKE ? AND office_users.status = 'REJECTED'", officeSearch) + } + }, + "rejectionReason": func(content string) func(*pop.Query) { + return func(query *pop.Query) { + rejectionSearch := fmt.Sprintf("%%%s%%", content) + query.Where("office_users.rejection_reason ILIKE ? AND office_users.status = 'REJECTED'", rejectionSearch) + } + }, + "rejectedOn": func(content string) func(*pop.Query) { + return func(query *pop.Query) { + RejectedOnSearch := fmt.Sprintf("%%%s%%", content) + query.Where("TO_CHAR(office_users.rejected_on , 'MM/DD/YYYY') ILIKE ? AND office_users.status = 'REJECTED'", RejectedOnSearch) + } + }, + "roles": func(content string) func(*pop.Query) { + return func(query *pop.Query) { + RoleSearch := fmt.Sprintf("%%%s%%", content) + query.Where("roles.role_name ILIKE ? AND office_users.status = 'REJECTED'", RoleSearch) } }, } diff --git a/pkg/handlers/adminapi/rejected_office_users_test.go b/pkg/handlers/adminapi/rejected_office_users_test.go index bf63d53a391..ab11e35764b 100644 --- a/pkg/handlers/adminapi/rejected_office_users_test.go +++ b/pkg/handlers/adminapi/rejected_office_users_test.go @@ -4,6 +4,8 @@ import ( "fmt" "net/http" "slices" + "strconv" + "strings" "time" "github.com/go-openapi/strfmt" @@ -54,6 +56,205 @@ func (suite *HandlerSuite) TestIndexRejectedOfficeUsersHandler() { suite.True(slices.Contains(actualID, expectedID)) } }) + + suite.Run("able to search by name & email", func() { + status := models.OfficeUserStatusREJECTED + rejectionReason := "Test rejection Reason" + rejectedOn := time.Date(2025, 03, 05, 1, 1, 1, 1, time.Local) + transportationOffice := factory.BuildTransportationOffice(suite.DB(), []factory.Customization{ + { + Model: models.TransportationOffice{ + Name: "JPPO Test Office", + }, + }, + }, nil) + factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + FirstName: "Angelina", + LastName: "Jolie", + Email: "laraCroft@mail.mil", + Status: &status, + }, + }, + }, []roles.RoleType{roles.RoleTypeTOO}) + factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + FirstName: "Billy", + LastName: "Bob", + Email: "bigBob@mail.mil", + Status: &status, + }, + }, + }, []roles.RoleType{roles.RoleTypeTIO}) + factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + FirstName: "Nick", + LastName: "Cage", + Email: "conAirKilluh@mail.mil", + Status: &status, + }, + }, + }, []roles.RoleType{roles.RoleTypeServicesCounselor}) + factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + FirstName: "Nick", + LastName: "Cage", + Email: "conAirKilluh2@mail.mil", + Status: &status, + TransportationOfficeID: transportationOffice.ID, + RejectionReason: &rejectionReason, + RejectedOn: &rejectedOn, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + }, []roles.RoleType{roles.RoleTypeServicesCounselor}) + + // partial first name search + nameSearch := "Nick" + filterJSON := fmt.Sprintf("{\"search\":\"%s\"}", nameSearch) + params := rejectedofficeuserop.IndexRejectedOfficeUsersParams{ + HTTPRequest: suite.setupAuthenticatedRequest("GET", "/rejected_office_users"), + Filter: &filterJSON, + } + + queryBuilder := query.NewQueryBuilder() + handler := IndexRejectedOfficeUsersHandler{ + HandlerConfig: suite.HandlerConfig(), + NewQueryFilter: query.NewQueryFilter, + RejectedOfficeUserListFetcher: rejectedofficeusers.NewRejectedOfficeUsersListFetcher(queryBuilder), + NewPagination: pagination.NewPagination, + } + + response := handler.Handle(params) + + suite.IsType(&rejectedofficeuserop.IndexRejectedOfficeUsersOK{}, response) + okResponse := response.(*rejectedofficeuserop.IndexRejectedOfficeUsersOK) + suite.Len(okResponse.Payload, 2) + suite.Equal(nameSearch, *okResponse.Payload[0].FirstName) + suite.Equal(nameSearch, *okResponse.Payload[1].FirstName) + + // email search + emailSearch := "conAirKilluh2" + filterJSON = fmt.Sprintf("{\"emails\":\"%s\"}", emailSearch) + params = rejectedofficeuserop.IndexRejectedOfficeUsersParams{ + HTTPRequest: suite.setupAuthenticatedRequest("GET", "/rejected_office_users"), + Filter: &filterJSON, + } + response = handler.Handle(params) + + suite.IsType(&rejectedofficeuserop.IndexRejectedOfficeUsersOK{}, response) + okResponse = response.(*rejectedofficeuserop.IndexRejectedOfficeUsersOK) + suite.Len(okResponse.Payload, 1) + + respEmail := *okResponse.Payload[0].Email + suite.Equal(emailSearch, respEmail[0:len(emailSearch)]) + suite.Equal(emailSearch, respEmail[0:len(emailSearch)]) + + // firstName search + firstSearch := "Angelina" + filterJSON = fmt.Sprintf("{\"firstName\":\"%s\"}", firstSearch) + params = rejectedofficeuserop.IndexRejectedOfficeUsersParams{ + HTTPRequest: suite.setupAuthenticatedRequest("GET", "/rejected_office_users"), + Filter: &filterJSON, + } + response = handler.Handle(params) + + suite.IsType(&rejectedofficeuserop.IndexRejectedOfficeUsersOK{}, response) + okResponse = response.(*rejectedofficeuserop.IndexRejectedOfficeUsersOK) + suite.Len(okResponse.Payload, 1) + suite.Equal(firstSearch, *okResponse.Payload[0].FirstName) + + // lastName search + lastSearch := "Cage" + filterJSON = fmt.Sprintf("{\"lastName\":\"%s\"}", lastSearch) + params = rejectedofficeuserop.IndexRejectedOfficeUsersParams{ + HTTPRequest: suite.setupAuthenticatedRequest("GET", "/rejected_office_users"), + Filter: &filterJSON, + } + response = handler.Handle(params) + + suite.IsType(&rejectedofficeuserop.IndexRejectedOfficeUsersOK{}, response) + okResponse = response.(*rejectedofficeuserop.IndexRejectedOfficeUsersOK) + suite.Len(okResponse.Payload, 2) + suite.Equal(lastSearch, *okResponse.Payload[0].LastName) + suite.Equal(lastSearch, *okResponse.Payload[1].LastName) + + // transportation office search + filterJSON = "{\"offices\":\"JPPO\"}" + params = rejectedofficeuserop.IndexRejectedOfficeUsersParams{ + HTTPRequest: suite.setupAuthenticatedRequest("GET", "/rejected_office_users"), + Filter: &filterJSON, + } + response = handler.Handle(params) + + suite.IsType(&rejectedofficeuserop.IndexRejectedOfficeUsersOK{}, response) + okResponse = response.(*rejectedofficeuserop.IndexRejectedOfficeUsersOK) + suite.Len(okResponse.Payload, 1) + suite.Equal(strfmt.UUID(transportationOffice.ID.String()), *okResponse.Payload[0].TransportationOfficeID) + + // rejection reason search + reasonSearch := "Test rejection" + filterJSON = fmt.Sprintf("{\"rejectionReason\":\"%s\"}", reasonSearch) + params = rejectedofficeuserop.IndexRejectedOfficeUsersParams{ + HTTPRequest: suite.setupAuthenticatedRequest("GET", "/rejected_office_users"), + Filter: &filterJSON, + } + response = handler.Handle(params) + + respRejection := *okResponse.Payload[0].RejectionReason + suite.IsType(&rejectedofficeuserop.IndexRejectedOfficeUsersOK{}, response) + okResponse = response.(*rejectedofficeuserop.IndexRejectedOfficeUsersOK) + suite.Len(okResponse.Payload, 1) + suite.Equal(reasonSearch, respRejection[0:len(reasonSearch)]) + + // rejectedOn search + rejectedOnSearch := "03" + filterJSON = fmt.Sprintf("{\"rejectedOn\":\"%s\"}", rejectedOnSearch) + params = rejectedofficeuserop.IndexRejectedOfficeUsersParams{ + HTTPRequest: suite.setupAuthenticatedRequest("GET", "/rejected_office_users"), + Filter: &filterJSON, + } + response = handler.Handle(params) + + suite.IsType(&rejectedofficeuserop.IndexRejectedOfficeUsersOK{}, response) + okResponse = response.(*rejectedofficeuserop.IndexRejectedOfficeUsersOK) + suite.Len(okResponse.Payload, 1) + + rejectedOnResp := okResponse.Payload[0].RejectedOn.String() + actualYear := strings.Split(rejectedOnResp, "-")[0] + actualMonth, err := strconv.Atoi(strings.Split(rejectedOnResp, "-")[1]) + + expectedYear := strconv.Itoa(rejectedOn.Year()) + expectedMonth := int(rejectedOn.Month()) + + suite.NoError(err) + suite.Equal(expectedYear, actualYear) + suite.Equal(expectedMonth, actualMonth) + + // roles search + roleSearch := "Services Counselor" + filterJSON = fmt.Sprintf("{\"roles\":\"%s\"}", roleSearch) + params = rejectedofficeuserop.IndexRejectedOfficeUsersParams{ + HTTPRequest: suite.setupAuthenticatedRequest("GET", "/rejected_office_users"), + Filter: &filterJSON, + } + response = handler.Handle(params) + + suite.IsType(&rejectedofficeuserop.IndexRejectedOfficeUsersOK{}, response) + okResponse = response.(*rejectedofficeuserop.IndexRejectedOfficeUsersOK) + suite.Len(okResponse.Payload, 2) + suite.Equal(roleSearch, *okResponse.Payload[0].Roles[0].RoleName) + suite.Equal(roleSearch, *okResponse.Payload[1].Roles[0].RoleName) + + }) } func (suite *HandlerSuite) TestGetRejectedOfficeUserHandler() { diff --git a/pkg/services/rejected_office_users/rejected_office_users_list_fetcher_test.go b/pkg/services/rejected_office_users/rejected_office_users_list_fetcher_test.go index 3c88bec2c52..85cd8e10e74 100644 --- a/pkg/services/rejected_office_users/rejected_office_users_list_fetcher_test.go +++ b/pkg/services/rejected_office_users/rejected_office_users_list_fetcher_test.go @@ -57,7 +57,7 @@ func (suite *RejectedOfficeUsersServiceSuite) TestFetchRejectedOfficeUserList() factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ { Model: models.OfficeUser{ - Status: &rejectedStatus, + Status: &rejectedStatus, }, }, }, []roles.RoleType{roles.RoleTypeTOO}) diff --git a/src/pages/Admin/RejectedOfficeUsers/RejectedOfficeUserList.jsx b/src/pages/Admin/RejectedOfficeUsers/RejectedOfficeUserList.jsx index 1a9c5f0825e..a912e938b0f 100644 --- a/src/pages/Admin/RejectedOfficeUsers/RejectedOfficeUserList.jsx +++ b/src/pages/Admin/RejectedOfficeUsers/RejectedOfficeUserList.jsx @@ -2,15 +2,19 @@ import React from 'react'; import { Datagrid, DateField, - Filter, List, ReferenceField, TextField, TextInput, TopToolbar, useRecordContext, + SearchInput, + FilterForm, + FilterButton, } from 'react-admin'; +import styles from './RejectedOfficeUserList.module.scss'; + import AdminPagination from 'scenes/SystemAdmin/shared/AdminPagination'; const RejectedOfficeUserShowRoles = () => { @@ -33,20 +37,36 @@ const ListActions = () => { return ; }; -const RejectedOfficeUserListFilter = () => ( - - - +const filterList = [ + , + , + , + , + , + , + , + , +]; + +const SearchFilters = () => ( +
+
+ +
+
+ +
+
); const defaultSort = { field: 'createdAt', order: 'DESC' }; const RejectedOfficeUserList = () => ( } pagination={} perPage={25} sort={defaultSort} - filters={} actions={} > diff --git a/src/pages/Admin/RejectedOfficeUsers/RejectedOfficeUserList.module.scss b/src/pages/Admin/RejectedOfficeUsers/RejectedOfficeUserList.module.scss new file mode 100644 index 00000000000..aa84a3f9627 --- /dev/null +++ b/src/pages/Admin/RejectedOfficeUsers/RejectedOfficeUserList.module.scss @@ -0,0 +1,14 @@ +@import 'shared/styles/colors.scss'; + +.searchContainer { + display: flex; + align-items: flex-end; + + .searchBar { + float: left; + } + + .filters { + padding-bottom: 5px; + } +} diff --git a/src/scenes/SystemAdmin/shared/AdminPagination.jsx b/src/scenes/SystemAdmin/shared/AdminPagination.jsx index 5afb920591f..72dd9f4de27 100644 --- a/src/scenes/SystemAdmin/shared/AdminPagination.jsx +++ b/src/scenes/SystemAdmin/shared/AdminPagination.jsx @@ -6,10 +6,7 @@ import styles from './AdminPagination.module.scss'; const AdminPagination = () => { const { isLoading, total } = useListContext(); return !isLoading && total === 0 ? ( -
- There are no results for this access code. Please check your entry to make sure you entered the correct letter - combination. -
+
No results found.
) : ( ); diff --git a/src/shared/Inputs/YesNoBoolen.test.jsx b/src/shared/Inputs/YesNoBoolen.test.jsx new file mode 100644 index 00000000000..2aef823bb8e --- /dev/null +++ b/src/shared/Inputs/YesNoBoolen.test.jsx @@ -0,0 +1,33 @@ +import YesNoBoolean from './YesNoBoolean'; +import { fireEvent, getByLabelText, render } from '@testing-library/react'; +import { Provider } from 'react-redux'; +import { configureStore } from 'shared/store'; + +const testProps = { + input: { + value: true, + onChange: jest.fn(), + }, +}; + +describe('YesNoBoolean', () => { + describe('with default props', () => { + it('renders without errors', () => { + const mockStore = configureStore({}); + render( + + + , + ); + }); + + it('renders without input', () => { + const mockStore = configureStore({}); + render( + + + , + ); + }); + }); +});