diff --git a/pkg/handlers/adminapi/api.go b/pkg/handlers/adminapi/api.go index 7e8a4183b1f..3aad8296570 100644 --- a/pkg/handlers/adminapi/api.go +++ b/pkg/handlers/adminapi/api.go @@ -97,7 +97,7 @@ func NewAdminAPI(handlerConfig handlers.HandlerConfig) *adminops.MymoveAPI { adminAPI.OfficeUsersIndexOfficeUsersHandler = IndexOfficeUsersHandler{ handlerConfig, - fetch.NewListFetcher(queryBuilder), + officeuser.NewOfficeUsersListFetcher(queryBuilder), query.NewQueryFilter, pagination.NewPagination, } diff --git a/pkg/handlers/adminapi/office_users.go b/pkg/handlers/adminapi/office_users.go index bb2bc65199c..88fe01d4e46 100644 --- a/pkg/handlers/adminapi/office_users.go +++ b/pkg/handlers/adminapi/office_users.go @@ -1,9 +1,12 @@ package adminapi import ( + "encoding/json" + "errors" "fmt" "github.com/go-openapi/runtime/middleware" + "github.com/gobuffalo/pop/v6" "github.com/gofrs/uuid" "go.uber.org/zap" @@ -108,18 +111,51 @@ func payloadForOfficeUserModel(o models.OfficeUser) *adminmessages.OfficeUser { // IndexOfficeUsersHandler returns a list of office users via GET /office_users type IndexOfficeUsersHandler struct { handlers.HandlerConfig - services.ListFetcher + services.OfficeUserListFetcher services.NewQueryFilter services.NewPagination } -var officeUserFilterConverters = map[string]func(string) []services.QueryFilter{ - "search": func(content string) []services.QueryFilter { - nameSearch := fmt.Sprintf("%s%%", content) - return []services.QueryFilter{ - query.NewQueryFilter("email", "ILIKE", fmt.Sprintf("%%%s%%", content)), - query.NewQueryFilter("first_name", "ILIKE", nameSearch), - query.NewQueryFilter("last_name", "ILIKE", nameSearch), +var officeUserFilterConverters = map[string]func(string) func(*pop.Query){ + "search": func(content string) func(*pop.Query) { + return func(query *pop.Query) { + firstSearch, lastSearch, emailSearch := fmt.Sprintf("%%%s%%", content), fmt.Sprintf("%%%s%%", content), fmt.Sprintf("%%%s%%", content) + query.Where("(office_users.first_name ILIKE ? OR office_users.last_name ILIKE ? OR office_users.email ILIKE ?) AND office_users.status = 'APPROVED'", firstSearch, lastSearch, emailSearch) + } + }, + "email": 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 = 'APPROVED'", emailSearch) + } + }, + "phone": func(content string) func(*pop.Query) { + return func(query *pop.Query) { + phoneSearch := fmt.Sprintf("%%%s%%", content) + query.Where("office_users.telephone ILIKE ? AND office_users.status = 'APPROVED'", phoneSearch) + } + }, + "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 = 'APPROVED'", 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 = 'APPROVED'", lastNameSearch) + } + }, + "office": 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 = 'APPROVED'", officeSearch) + } + }, + "active": func(content string) func(*pop.Query) { + return func(query *pop.Query) { + query.Where("office_users.active = ? AND office_users.status = 'APPROVED'", content) } }, } @@ -128,27 +164,25 @@ var officeUserFilterConverters = map[string]func(string) []services.QueryFilter{ func (h IndexOfficeUsersHandler) Handle(params officeuserop.IndexOfficeUsersParams) middleware.Responder { return h.AuditableAppContextFromRequestWithErrors(params.HTTPRequest, func(appCtx appcontext.AppContext) (middleware.Responder, error) { - // Here is where NewQueryFilter will be used to create Filters from the 'filter' query param - queryFilters := generateQueryFilters(appCtx.Logger(), params.Filter, officeUserFilterConverters) + var filtersMap map[string]string + if params.Filter != nil && *params.Filter != "" { + err := json.Unmarshal([]byte(*params.Filter), &filtersMap) + if err != nil { + return handlers.ResponseForError(appCtx.Logger(), errors.New("invalid filter format")), err + } + } - // Add a filter for approved status - queryFilters = append(queryFilters, query.NewQueryFilter("status", "=", "APPROVED")) + var filterFuncs []func(*pop.Query) + for key, filterFunc := range officeUserFilterConverters { + if filterValue, exists := filtersMap[key]; exists { + filterFuncs = append(filterFuncs, filterFunc(filterValue)) + } + } pagination := h.NewPagination(params.Page, params.PerPage) ordering := query.NewQueryOrder(params.Sort, params.Order) - queryAssociations := query.NewQueryAssociationsPreload([]services.QueryAssociation{ - query.NewQueryAssociation("User.Roles"), - query.NewQueryAssociation("User.Privileges"), - }) - - var officeUsers models.OfficeUsers - err := h.ListFetcher.FetchRecordList(appCtx, &officeUsers, queryFilters, queryAssociations, pagination, ordering) - if err != nil { - return handlers.ResponseForError(appCtx.Logger(), err), err - } - - totalOfficeUsersCount, err := h.ListFetcher.FetchRecordCount(appCtx, &officeUsers, queryFilters) + officeUsers, count, err := h.OfficeUserListFetcher.FetchOfficeUsersList(appCtx, filterFuncs, pagination, ordering) if err != nil { return handlers.ResponseForError(appCtx.Logger(), err), err } @@ -161,7 +195,7 @@ func (h IndexOfficeUsersHandler) Handle(params officeuserop.IndexOfficeUsersPara payload[i] = payloadForOfficeUserModel(s) } - return officeuserop.NewIndexOfficeUsersOK().WithContentRange(fmt.Sprintf("office users %d-%d/%d", pagination.Offset(), pagination.Offset()+queriedOfficeUsersCount, totalOfficeUsersCount)).WithPayload(payload), nil + return officeuserop.NewIndexOfficeUsersOK().WithContentRange(fmt.Sprintf("office users %d-%d/%d", pagination.Offset(), pagination.Offset()+queriedOfficeUsersCount, count)).WithPayload(payload), nil }) } diff --git a/pkg/handlers/adminapi/office_users_test.go b/pkg/handlers/adminapi/office_users_test.go index b2386b96a2e..6f2fe2599aa 100644 --- a/pkg/handlers/adminapi/office_users_test.go +++ b/pkg/handlers/adminapi/office_users_test.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "net/http/httptest" + "slices" "github.com/go-openapi/strfmt" "github.com/gobuffalo/validate/v3" @@ -19,7 +20,6 @@ import ( "github.com/transcom/mymove/pkg/models" "github.com/transcom/mymove/pkg/models/roles" "github.com/transcom/mymove/pkg/services" - fetch "github.com/transcom/mymove/pkg/services/fetch" "github.com/transcom/mymove/pkg/services/mocks" officeuser "github.com/transcom/mymove/pkg/services/office_user" "github.com/transcom/mymove/pkg/services/pagination" @@ -31,36 +31,41 @@ import ( ) func (suite *HandlerSuite) TestIndexOfficeUsersHandler() { - setupTestData := func() models.OfficeUsers { - return models.OfficeUsers{ + // test that everything is wired up + suite.Run("integration test ok response", func() { + officeUsers := models.OfficeUsers{ factory.BuildOfficeUserWithRoles(suite.DB(), factory.GetTraitApprovedOfficeUser(), []roles.RoleType{roles.RoleTypeQae}), factory.BuildOfficeUserWithRoles(suite.DB(), factory.GetTraitApprovedOfficeUser(), []roles.RoleType{roles.RoleTypeQae}), factory.BuildOfficeUserWithRoles(suite.DB(), factory.GetTraitApprovedOfficeUser(), []roles.RoleType{roles.RoleTypeQae, roles.RoleTypeQae, roles.RoleTypeCustomer, roles.RoleTypeContractingOfficer, roles.RoleTypeContractingOfficer}), } - } - - // test that everything is wired up - suite.Run("integration test ok response", func() { - officeUsers := setupTestData() params := officeuserop.IndexOfficeUsersParams{ HTTPRequest: suite.setupAuthenticatedRequest("GET", "/office_users"), } queryBuilder := query.NewQueryBuilder() handler := IndexOfficeUsersHandler{ - HandlerConfig: suite.HandlerConfig(), - NewQueryFilter: query.NewQueryFilter, - ListFetcher: fetch.NewListFetcher(queryBuilder), - NewPagination: pagination.NewPagination, + HandlerConfig: suite.HandlerConfig(), + NewQueryFilter: query.NewQueryFilter, + OfficeUserListFetcher: officeuser.NewOfficeUsersListFetcher(queryBuilder), + NewPagination: pagination.NewPagination, } response := handler.Handle(params) suite.IsType(&officeuserop.IndexOfficeUsersOK{}, response) okResponse := response.(*officeuserop.IndexOfficeUsersOK) - suite.Len(okResponse.Payload, 3) - suite.Equal(officeUsers[0].ID.String(), okResponse.Payload[0].ID.String()) - suite.Equal(string(officeUsers[0].User.Roles[0].RoleType), *okResponse.Payload[0].Roles[0].RoleType) + + actualOfficeUsers := okResponse.Payload + suite.Equal(len(officeUsers), len(actualOfficeUsers)) + + expectedOfficeUser1Id := officeUsers[0].ID.String() + expectedOfficeUser2Id := officeUsers[1].ID.String() + expectedOfficeUser3Id := officeUsers[2].ID.String() + expectedOfficeUserIDs := []string{expectedOfficeUser1Id, expectedOfficeUser2Id, expectedOfficeUser3Id} + + for i := 0; i < len(actualOfficeUsers); i++ { + suite.True(slices.Contains(expectedOfficeUserIDs, actualOfficeUsers[i].ID.String())) + } }) // Test that user roles list is not returning duplicate roles @@ -74,10 +79,10 @@ func (suite *HandlerSuite) TestIndexOfficeUsersHandler() { queryBuilder := query.NewQueryBuilder() handler := IndexOfficeUsersHandler{ - HandlerConfig: suite.HandlerConfig(), - NewQueryFilter: query.NewQueryFilter, - ListFetcher: fetch.NewListFetcher(queryBuilder), - NewPagination: pagination.NewPagination, + HandlerConfig: suite.HandlerConfig(), + NewQueryFilter: query.NewQueryFilter, + OfficeUserListFetcher: officeuser.NewOfficeUsersListFetcher(queryBuilder), + NewPagination: pagination.NewPagination, } response := handler.Handle(params) @@ -105,12 +110,8 @@ func (suite *HandlerSuite) TestIndexOfficeUsersHandler() { suite.Len(officeUsers[0].User.Roles, 3) }) - suite.Run("fetch return an empty list", func() { - setupTestData() - // TEST: IndexOfficeUserHandler, Fetcher - // Set up: Provide an invalid search that won't be found - // Expected Outcome: An empty list is returned and we get a 200 OK. - fakeFilter := "{\"search\":\"something\"}" + suite.Run("invalid search returns no results", func() { + fakeFilter := "{\"search\":\"invalidSearch\"}" params := officeuserop.IndexOfficeUsersParams{ HTTPRequest: suite.setupAuthenticatedRequest("GET", "/office_users"), @@ -119,10 +120,10 @@ func (suite *HandlerSuite) TestIndexOfficeUsersHandler() { queryBuilder := query.NewQueryBuilder() handler := IndexOfficeUsersHandler{ - HandlerConfig: suite.HandlerConfig(), - ListFetcher: fetch.NewListFetcher(queryBuilder), - NewQueryFilter: query.NewQueryFilter, - NewPagination: pagination.NewPagination, + HandlerConfig: suite.HandlerConfig(), + OfficeUserListFetcher: officeuser.NewOfficeUsersListFetcher(queryBuilder), + NewQueryFilter: query.NewQueryFilter, + NewPagination: pagination.NewPagination, } response := handler.Handle(params) @@ -130,6 +131,167 @@ func (suite *HandlerSuite) TestIndexOfficeUsersHandler() { suite.Len(okResponse.Payload, 0) }) + + suite.Run("able to search and filter", func() { + status := models.OfficeUserStatusAPPROVED + 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, + Telephone: "555-555-5555", + }, + }, + }, []roles.RoleType{roles.RoleTypeTOO}) + factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + FirstName: "Billy", + LastName: "Bob", + Email: "bigBob@mail.mil", + Status: &status, + Telephone: "555-555-5555", + }, + }, + }, []roles.RoleType{roles.RoleTypeTIO}) + factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + FirstName: "Nick", + LastName: "Cage", + Email: "conAirKilluh@mail.mil", + Status: &status, + Telephone: "555-555-5555", + }, + }, + }, []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, + Telephone: "415-555-5555", + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + }, []roles.RoleType{roles.RoleTypeServicesCounselor}) + + // partial name search + nameSearch := "Nick" + filterJSON := fmt.Sprintf("{\"search\":\"%s\"}", nameSearch) + params := officeuserop.IndexOfficeUsersParams{ + HTTPRequest: suite.setupAuthenticatedRequest("GET", "/office_users"), + Filter: &filterJSON, + } + + queryBuilder := query.NewQueryBuilder() + handler := IndexOfficeUsersHandler{ + HandlerConfig: suite.HandlerConfig(), + NewQueryFilter: query.NewQueryFilter, + OfficeUserListFetcher: officeuser.NewOfficeUsersListFetcher(queryBuilder), + NewPagination: pagination.NewPagination, + } + + response := handler.Handle(params) + + suite.IsType(&officeuserop.IndexOfficeUsersOK{}, response) + okResponse := response.(*officeuserop.IndexOfficeUsersOK) + 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("{\"email\":\"%s\"}", emailSearch) + params = officeuserop.IndexOfficeUsersParams{ + HTTPRequest: suite.setupAuthenticatedRequest("GET", "/office_users"), + Filter: &filterJSON, + } + response = handler.Handle(params) + + suite.IsType(&officeuserop.IndexOfficeUsersOK{}, response) + okResponse = response.(*officeuserop.IndexOfficeUsersOK) + suite.Len(okResponse.Payload, 1) + + respEmail := *okResponse.Payload[0].Email + suite.Equal(emailSearch, respEmail[0:len(emailSearch)]) + suite.Equal(emailSearch, respEmail[0:len(emailSearch)]) + + // telephone search + phoneSearch := "415-" + filterJSON = fmt.Sprintf("{\"phone\":\"%s\"}", phoneSearch) + params = officeuserop.IndexOfficeUsersParams{ + HTTPRequest: suite.setupAuthenticatedRequest("GET", "/office_users"), + Filter: &filterJSON, + } + response = handler.Handle(params) + + suite.IsType(&officeuserop.IndexOfficeUsersOK{}, response) + okResponse = response.(*officeuserop.IndexOfficeUsersOK) + suite.Len(okResponse.Payload, 1) + + respPhone := *okResponse.Payload[0].Telephone + suite.Equal(phoneSearch, respPhone[0:len(phoneSearch)]) + + // firstName search + firstSearch := "Angelina" + filterJSON = fmt.Sprintf("{\"firstName\":\"%s\"}", firstSearch) + params = officeuserop.IndexOfficeUsersParams{ + HTTPRequest: suite.setupAuthenticatedRequest("GET", "/office_users"), + Filter: &filterJSON, + } + response = handler.Handle(params) + + suite.IsType(&officeuserop.IndexOfficeUsersOK{}, response) + okResponse = response.(*officeuserop.IndexOfficeUsersOK) + suite.Len(okResponse.Payload, 1) + suite.Equal(firstSearch, *okResponse.Payload[0].FirstName) + + // lastName search + lastSearch := "Cage" + filterJSON = fmt.Sprintf("{\"lastName\":\"%s\"}", lastSearch) + params = officeuserop.IndexOfficeUsersParams{ + HTTPRequest: suite.setupAuthenticatedRequest("GET", "/office_users"), + Filter: &filterJSON, + } + response = handler.Handle(params) + + suite.IsType(&officeuserop.IndexOfficeUsersOK{}, response) + okResponse = response.(*officeuserop.IndexOfficeUsersOK) + suite.Len(okResponse.Payload, 2) + suite.Equal(lastSearch, *okResponse.Payload[0].LastName) + suite.Equal(lastSearch, *okResponse.Payload[1].LastName) + + // transportation office search + filterJSON = "{\"office\":\"JPPO\"}" + params = officeuserop.IndexOfficeUsersParams{ + HTTPRequest: suite.setupAuthenticatedRequest("GET", "/office_users"), + Filter: &filterJSON, + } + response = handler.Handle(params) + + suite.IsType(&officeuserop.IndexOfficeUsersOK{}, response) + okResponse = response.(*officeuserop.IndexOfficeUsersOK) + suite.Len(okResponse.Payload, 1) + suite.Equal(strfmt.UUID(transportationOffice.ID.String()), *okResponse.Payload[0].TransportationOfficeID) + + }) } func (suite *HandlerSuite) TestGetOfficeUserHandler() { diff --git a/pkg/services/office_user.go b/pkg/services/office_user.go index ac6b4f23f75..3b007c8a0f2 100644 --- a/pkg/services/office_user.go +++ b/pkg/services/office_user.go @@ -1,6 +1,7 @@ package services import ( + "github.com/gobuffalo/pop/v6" "github.com/gobuffalo/validate/v3" "github.com/gofrs/uuid" @@ -10,6 +11,14 @@ import ( "github.com/transcom/mymove/pkg/models/roles" ) +// OfficeUserListFetcher is the exported interface for fetching multiple office users +// +//go:generate mockery --name OfficeUserListFetcher +type OfficeUserListFetcher interface { + FetchOfficeUsersList(appCtx appcontext.AppContext, filterFuncs []func(*pop.Query), pagination Pagination, ordering QueryOrder) (models.OfficeUsers, int, error) + FetchOfficeUsersCount(appCtx appcontext.AppContext, filters []QueryFilter) (int, error) +} + // OfficeUserFetcher is the exported interface for fetching a single office user // //go:generate mockery --name OfficeUserFetcher diff --git a/pkg/services/office_user/office_users_list_fetcher.go b/pkg/services/office_user/office_users_list_fetcher.go new file mode 100644 index 00000000000..d3b180004aa --- /dev/null +++ b/pkg/services/office_user/office_users_list_fetcher.go @@ -0,0 +1,86 @@ +package officeuser + +import ( + "fmt" + "sort" + + "github.com/gobuffalo/pop/v6" + + "github.com/transcom/mymove/pkg/appcontext" + "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/services" +) + +type officeUsersListQueryBuilder interface { + Count(appCtx appcontext.AppContext, model interface{}, filters []services.QueryFilter) (int, error) +} + +type officeUserListFetcher struct { + builder officeUsersListQueryBuilder +} + +// FetchOfficeUserList uses the passed query builder to fetch a list of office users +func (o *officeUserListFetcher) FetchOfficeUsersList(appCtx appcontext.AppContext, filterFuncs []func(*pop.Query), pagination services.Pagination, ordering services.QueryOrder) (models.OfficeUsers, int, error) { + var query *pop.Query + var officeUsers models.OfficeUsers + + query = appCtx.DB().Q().EagerPreload( + "User.Roles", + "TransportationOffice"). + Join("users", "users.id = office_users.user_id"). + Join("users_roles", "users.id = users_roles.user_id"). + Join("roles", "users_roles.role_id = roles.id"). + Join("transportation_offices", "office_users.transportation_office_id = transportation_offices.id") + + for _, filterFunc := range filterFuncs { + filterFunc(query) + } + + query = query.Where("status = ?", models.OfficeUserStatusAPPROVED) + query.GroupBy("office_users.id") + + var order = "desc" + if ordering.SortOrder() != nil && *ordering.SortOrder() { + order = "asc" + } + + var orderTerm = "id" + if ordering.Column() != nil { + orderTerm = *ordering.Column() + } + + query.Order(fmt.Sprintf("%s %s", orderTerm, order)) + query.Select("office_users.*") + + err := query.Paginate(pagination.Page(), pagination.PerPage()).All(&officeUsers) + if err != nil { + return nil, 0, err + } + + if orderTerm == "transportation_office_id" { + if order == "desc" { + sort.Slice(officeUsers, func(i, j int) bool { + return officeUsers[i].TransportationOffice.Name > officeUsers[j].TransportationOffice.Name + }) + } else { + sort.Slice(officeUsers, func(i, j int) bool { + return officeUsers[i].TransportationOffice.Name < officeUsers[j].TransportationOffice.Name + }) + } + } + + count := query.Paginator.TotalEntriesSize + return officeUsers, count, nil +} + +// FetchOfficeUserList uses the passed query builder to fetch a list of office users +func (o *officeUserListFetcher) FetchOfficeUsersCount(appCtx appcontext.AppContext, filters []services.QueryFilter) (int, error) { + var officeUsers models.OfficeUsers + count, err := o.builder.Count(appCtx, &officeUsers, filters) + return count, err +} + +// NewOfficecUserListFetcher returns an implementation of OfficeUserListFetcher +func NewOfficeUsersListFetcher(builder officeUsersListQueryBuilder) services.OfficeUserListFetcher { + return &officeUserListFetcher{builder} +} diff --git a/pkg/services/office_user/office_users_list_fetcher_test.go b/pkg/services/office_user/office_users_list_fetcher_test.go new file mode 100644 index 00000000000..69b26a4fa42 --- /dev/null +++ b/pkg/services/office_user/office_users_list_fetcher_test.go @@ -0,0 +1,142 @@ +package officeuser + +import ( + "github.com/transcom/mymove/pkg/appcontext" + "github.com/transcom/mymove/pkg/factory" + "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/models/roles" + "github.com/transcom/mymove/pkg/services" + "github.com/transcom/mymove/pkg/services/pagination" + "github.com/transcom/mymove/pkg/services/query" +) + +type testOfficeUsersListQueryBuilder struct { + fakeCount func(appCtx appcontext.AppContext, model interface{}) (int, error) +} + +func (t *testOfficeUsersListQueryBuilder) Count(appCtx appcontext.AppContext, model interface{}, _ []services.QueryFilter) (int, error) { + count, m := t.fakeCount(appCtx, model) + return count, m +} + +func defaultPagination() services.Pagination { + page, perPage := pagination.DefaultPage(), pagination.DefaultPerPage() + return pagination.NewPagination(&page, &perPage) +} + +func defaultOrdering() services.QueryOrder { + return query.NewQueryOrder(nil, nil) +} + +func (suite *OfficeUserServiceSuite) TestFetchOfficeUserList() { + suite.Run("if the users are successfully fetched, they should be returned", func() { + status := models.OfficeUserStatusAPPROVED + officeUser1 := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + Status: &status, + }, + }, + }, []roles.RoleType{roles.RoleTypeTOO}) + builder := &testOfficeUsersListQueryBuilder{} + + fetcher := NewOfficeUsersListFetcher(builder) + + officeUsers, _, err := fetcher.FetchOfficeUsersList(suite.AppContextForTest(), nil, defaultPagination(), defaultOrdering()) + + suite.NoError(err) + suite.Equal(officeUser1.ID, officeUsers[0].ID) + }) + + suite.Run("if there are no office users, we don't receive any office users", func() { + builder := &testOfficeUsersListQueryBuilder{} + + fetcher := NewOfficeUsersListFetcher(builder) + + officeUsers, _, err := fetcher.FetchOfficeUsersList(suite.AppContextForTest(), nil, defaultPagination(), defaultOrdering()) + + suite.NoError(err) + suite.Equal(models.OfficeUsers(nil), officeUsers) + }) + + suite.Run("should sort and order office users", func() { + status := models.OfficeUserStatusAPPROVED + officeUser1 := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + FirstName: "Angelina", + LastName: "Jolie", + Email: "laraCroft@mail.mil", + Status: &status, + }, + }, + { + Model: models.TransportationOffice{ + Name: "PPPO Kirtland AFB - USAF", + }, + }, + }, []roles.RoleType{roles.RoleTypeTOO}) + officeUser2 := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + FirstName: "Billy", + LastName: "Bob", + Email: "bigBob@mail.mil", + Status: &status, + }, + }, + { + Model: models.TransportationOffice{ + Name: "PPPO Fort Knox - USA", + }, + }, + }, []roles.RoleType{roles.RoleTypeTIO}) + officeUser3 := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + FirstName: "Nick", + LastName: "Cage", + Email: "conAirKilluh@mail.mil", + Status: &status, + }, + }, + { + Model: models.TransportationOffice{ + Name: "PPPO Detroit Arsenal - USA", + }, + }, + }, []roles.RoleType{roles.RoleTypeServicesCounselor}) + + builder := &testOfficeUsersListQueryBuilder{} + + fetcher := NewOfficeUsersListFetcher(builder) + + column := "transportation_office_id" + ordering := query.NewQueryOrder(&column, models.BoolPointer(true)) + + officeUsers, _, err := fetcher.FetchOfficeUsersList(suite.AppContextForTest(), nil, defaultPagination(), ordering) + + suite.NoError(err) + suite.Len(officeUsers, 3) + suite.Equal(officeUser3.ID.String(), officeUsers[0].ID.String()) + suite.Equal(officeUser2.ID.String(), officeUsers[1].ID.String()) + suite.Equal(officeUser1.ID.String(), officeUsers[2].ID.String()) + + ordering = query.NewQueryOrder(&column, models.BoolPointer(false)) + + officeUsers, _, err = fetcher.FetchOfficeUsersList(suite.AppContextForTest(), nil, defaultPagination(), ordering) + + suite.NoError(err) + suite.Len(officeUsers, 3) + suite.Equal(officeUser1.ID.String(), officeUsers[0].ID.String()) + suite.Equal(officeUser2.ID.String(), officeUsers[1].ID.String()) + suite.Equal(officeUser3.ID.String(), officeUsers[2].ID.String()) + + column = "unknown_column" + + officeUsers, _, err = fetcher.FetchOfficeUsersList(suite.AppContextForTest(), nil, defaultPagination(), ordering) + + suite.Error(err) + suite.Len(officeUsers, 0) + }) +} diff --git a/src/pages/Admin/OfficeUsers/OfficeUserList.jsx b/src/pages/Admin/OfficeUsers/OfficeUserList.jsx index 5cbdca8aa98..64d75083ace 100644 --- a/src/pages/Admin/OfficeUsers/OfficeUserList.jsx +++ b/src/pages/Admin/OfficeUsers/OfficeUserList.jsx @@ -4,7 +4,9 @@ import { CreateButton, Datagrid, ExportButton, - Filter, + SearchInput, + FilterForm, + FilterButton, List, ReferenceField, TextField, @@ -15,6 +17,8 @@ import { } from 'react-admin'; import * as jsonexport from 'jsonexport/dist'; +import styles from './OfficeUserList.module.scss'; + import ImportOfficeUserButton from 'components/Admin/ImportOfficeUserButton'; import AdminPagination from 'scenes/SystemAdmin/shared/AdminPagination'; @@ -60,10 +64,25 @@ const ListActions = () => { ); }; -const OfficeUserListFilter = () => ( - - - +const filterList = [ + , + , + , + , + , + , + , +]; + +const SearchFilters = () => ( +
+
+ +
+
+ +
+
); const defaultSort = { field: 'last_name', order: 'ASC' }; @@ -73,7 +92,7 @@ const OfficeUserList = () => ( pagination={} perPage={25} sort={defaultSort} - filters={} + filters={} actions={} > diff --git a/src/pages/Admin/OfficeUsers/OfficeUserList.module.scss b/src/pages/Admin/OfficeUsers/OfficeUserList.module.scss new file mode 100644 index 00000000000..2fe7c83a5fd --- /dev/null +++ b/src/pages/Admin/OfficeUsers/OfficeUserList.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; + } +} \ No newline at end of file diff --git a/src/store/entities/selectors.test.js b/src/store/entities/selectors.test.js index 259621b26b5..9e96e147caf 100644 --- a/src/store/entities/selectors.test.js +++ b/src/store/entities/selectors.test.js @@ -18,6 +18,7 @@ import { selectCanAddOrders, selectMoveId, selectUbAllowance, + selectProGearWeightTicketAndIndexById, } from './selectors'; import { profileStates } from 'constants/customerStates'; @@ -1806,3 +1807,128 @@ describe('selectUbAllowance', () => { expect(selectUbAllowance(testState)).toEqual(null); }); }); + +describe('selectProGearWeightTicketAndIndexById', () => { + const proGearWeightId = '71422b71-a40b-41a7-b2ff-4da922a9c7f2'; + const testState = { + entities: { + mtoShipments: { + '2ed2998e-ae36-46cd-af83-c3ecee55fe3e': { + createdAt: '2022-07-01T01:10:51.224Z', + eTag: 'MjAyMi0wNy0xMVQxODoyMDoxOC43MjQ1NzRa', + id: '2ed2998e-ae36-46cd-af83-c3ecee55fe3e', + moveTaskOrderID: '26b960d8-a96d-4450-a441-673ccd7cc3c7', + ppmShipment: { + actualDestinationPostalCode: '30813', + actualMoveDate: '2022-07-31', + actualPickupPostalCode: '90210', + advanceAmountReceived: null, + advanceAmountRequested: 598700, + approvedAt: '2022-04-15T12:30:00.000Z', + createdAt: '2022-07-01T01:10:51.231Z', + eTag: 'MjAyMi0wNy0xMVQxODoyMDoxOC43NTIwMDNa', + estimatedIncentive: 1000000, + estimatedWeight: 4000, + expectedDepartureDate: '2020-03-15', + hasProGear: true, + hasReceivedAdvance: false, + hasRequestedAdvance: true, + id: 'b9ae4c25-1376-4b9b-8781-106b5ae7ecab', + proGearWeight: 1987, + reviewedAt: null, + shipmentId: '2ed2998e-ae36-46cd-af83-c3ecee55fe3e', + sitEstimatedCost: null, + sitEstimatedDepartureDate: null, + sitEstimatedEntryDate: null, + sitEstimatedWeight: null, + sitExpected: false, + spouseProGearWeight: 498, + status: 'WAITING_ON_CUSTOMER', + submittedAt: null, + updatedAt: '2022-07-11T18:20:18.752Z', + proGearWeightTickets: [ + { + id: 'd35d835f-8258-4266-87aa-54d61c917780', + emptyWeightDocumentId: '000676ac-c5ff-4630-8768-ef238f04e706', + fullWeightDocumentId: '7eeb270b-dc97-4f95-94c3-709c082cbf94', + }, + { + id: proGearWeightId, + emptyWeightDocumentId: '15fdd562-82a9-4892-85d7-81cc3a85e68e', + fullWeightDocumentId: '4a7f7fd9-15d1-468f-9184-53d7c0c1ccdc', + }, + ], + }, + shipmentType: 'PPM', + status: 'APPROVED', + updatedAt: '2022-07-11T18:20:18.724Z', + }, + }, + }, + }; + const mtoShipmentID = Object.keys(testState.entities.mtoShipments)[0]; + + it('returns pro gear weight tickets if exists', () => { + expect(selectProGearWeightTicketAndIndexById(testState, mtoShipmentID, proGearWeightId)).toEqual({ + proGearWeightTicket: testState.entities.mtoShipments[mtoShipmentID].ppmShipment.proGearWeightTickets[1], + index: 1, + }); + }); +}); + +describe('selectProGearWeightTicketAndIndexById', () => { + const proGearWeightId = '71422b71-a40b-41a7-b2ff-4da922a9c7f2'; + const testState = { + entities: { + mtoShipments: { + '2ed2998e-ae36-46cd-af83-c3ecee55fe3e': { + createdAt: '2022-07-01T01:10:51.224Z', + eTag: 'MjAyMi0wNy0xMVQxODoyMDoxOC43MjQ1NzRa', + id: '2ed2998e-ae36-46cd-af83-c3ecee55fe3e', + moveTaskOrderID: '26b960d8-a96d-4450-a441-673ccd7cc3c7', + ppmShipment: { + actualDestinationPostalCode: '30813', + actualMoveDate: '2022-07-31', + actualPickupPostalCode: '90210', + advanceAmountReceived: null, + advanceAmountRequested: 598700, + approvedAt: '2022-04-15T12:30:00.000Z', + createdAt: '2022-07-01T01:10:51.231Z', + eTag: 'MjAyMi0wNy0xMVQxODoyMDoxOC43NTIwMDNa', + estimatedIncentive: 1000000, + estimatedWeight: 4000, + expectedDepartureDate: '2020-03-15', + hasProGear: true, + hasReceivedAdvance: false, + hasRequestedAdvance: true, + id: 'b9ae4c25-1376-4b9b-8781-106b5ae7ecab', + proGearWeight: 1987, + reviewedAt: null, + shipmentId: '2ed2998e-ae36-46cd-af83-c3ecee55fe3e', + sitEstimatedCost: null, + sitEstimatedDepartureDate: null, + sitEstimatedEntryDate: null, + sitEstimatedWeight: null, + sitExpected: false, + spouseProGearWeight: 498, + status: 'WAITING_ON_CUSTOMER', + submittedAt: null, + updatedAt: '2022-07-11T18:20:18.752Z', + proGearWeightTickets: [], + }, + shipmentType: 'PPM', + status: 'APPROVED', + updatedAt: '2022-07-11T18:20:18.724Z', + }, + }, + }, + }; + const mtoShipmentID = Object.keys(testState.entities.mtoShipments)[0]; + + it('returns null if no pro gear weight tickets exist', () => { + expect(selectProGearWeightTicketAndIndexById(testState, mtoShipmentID, proGearWeightId)).toEqual({ + proGearWeightTicket: null, + index: -1, + }); + }); +});