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 = () => (
-