diff --git a/pkg/factory/move_factory.go b/pkg/factory/move_factory.go index e2192ae93f0..15b0fb4154c 100644 --- a/pkg/factory/move_factory.go +++ b/pkg/factory/move_factory.go @@ -35,6 +35,22 @@ func BuildMove(db *pop.Connection, customs []Customization, traits []Trait) mode closeoutOffice = BuildTransportationOffice(db, tempCloseoutOfficeCustoms, nil) } + var counselingOffice models.TransportationOffice + tempCounselingOfficeCustoms := customs + counselingOfficeResult := findValidCustomization(customs, TransportationOffices.CounselingOffice) + if counselingOfficeResult != nil { + tempCounselingOfficeCustoms = convertCustomizationInList(tempCounselingOfficeCustoms, TransportationOffices.CounselingOffice, TransportationOffice) + counselingOffice = BuildTransportationOffice(db, tempCounselingOfficeCustoms, nil) + } + + var scAssignedUser models.OfficeUser + tempSCAssignedUserCustoms := customs + scAssignedUserResult := findValidCustomization(customs, OfficeUsers.SCAssignedUser) + if scAssignedUserResult != nil { + tempSCAssignedUserCustoms = convertCustomizationInList(tempSCAssignedUserCustoms, OfficeUsers.SCAssignedUser, OfficeUser) + scAssignedUser = BuildOfficeUser(db, tempSCAssignedUserCustoms, nil) + } + var defaultReferenceID string var err error if db != nil { @@ -73,6 +89,16 @@ func BuildMove(db *pop.Connection, customs []Customization, traits []Trait) mode move.CloseoutOfficeID = &closeoutOffice.ID } + if counselingOfficeResult != nil { + move.CounselingOffice = &counselingOffice + move.CounselingOfficeID = &counselingOffice.ID + } + + if scAssignedUserResult != nil { + move.SCAssignedUser = &scAssignedUser + move.SCAssignedID = &scAssignedUser.ID + } + // Overwrite values with those from assertions testdatagen.MergeModels(&move, cMove) @@ -165,6 +191,8 @@ func BuildMoveWithShipment(db *pop.Connection, customs []Customization, traits [ return move } func BuildMoveWithPPMShipment(db *pop.Connection, customs []Customization, traits []Trait) models.Move { + // Please note this function runs BuildMove 3 times + // Once here, once in buildMTOShipmentWithBuildType, and once in BuildPPMShipment move := BuildMove(db, customs, traits) mtoShipment := buildMTOShipmentWithBuildType(db, customs, traits, mtoShipmentPPM) diff --git a/pkg/factory/move_factory_test.go b/pkg/factory/move_factory_test.go index 4b8a86a3df0..306e7f6ee44 100644 --- a/pkg/factory/move_factory_test.go +++ b/pkg/factory/move_factory_test.go @@ -4,6 +4,7 @@ import ( "time" "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/models/roles" ) func (suite *FactorySuite) TestBuildMove() { @@ -258,6 +259,23 @@ func (suite *FactorySuite) TestBuildMove() { suite.NotNil(move.AvailableToPrimeAt) suite.NotNil(move.ApprovedAt) }) + suite.Run("Successful creation of a move with an assigned SC", func() { + officeUser := BuildOfficeUserWithRoles(suite.DB(), nil, []roles.RoleType{roles.RoleTypeServicesCounselor}) + + move := BuildMoveWithShipment(suite.DB(), []Customization{ + { + Model: models.Move{ + Status: models.MoveStatusNeedsServiceCounseling, + }, + }, + { + Model: officeUser, + LinkOnly: true, + Type: &OfficeUsers.SCAssignedUser, + }, + }, nil) + suite.Equal(officeUser.ID, *move.SCAssignedID) + }) suite.Run("Successful creation of move with shipment", func() { // Under test: BuildMoveWithShipment // Set up: Create a move using BuildMoveWithShipment diff --git a/pkg/factory/office_user_factory.go b/pkg/factory/office_user_factory.go index bf2298003ed..09b8bb0b154 100644 --- a/pkg/factory/office_user_factory.go +++ b/pkg/factory/office_user_factory.go @@ -37,8 +37,25 @@ func BuildOfficeUser(db *pop.Connection, customs []Customization, traits []Trait // Find/create the user model user := BuildUserAndUsersRoles(db, customs, nil) - // Find/create the TransportationOffice model - transportationOffice := BuildTransportationOffice(db, customs, nil) + var transportationOffice models.TransportationOffice + + tempCloseoutOfficeCustoms := customs + closeoutOfficeResult := findValidCustomization(customs, TransportationOffices.CloseoutOffice) + if closeoutOfficeResult != nil { + tempCloseoutOfficeCustoms = convertCustomizationInList(tempCloseoutOfficeCustoms, TransportationOffices.CloseoutOffice, TransportationOffice) + transportationOffice = BuildTransportationOffice(db, tempCloseoutOfficeCustoms, nil) + } + + tempCounselingOfficeCustoms := customs + counselingOfficeResult := findValidCustomization(customs, TransportationOffices.CounselingOffice) + if counselingOfficeResult != nil { + tempCounselingOfficeCustoms = convertCustomizationInList(tempCounselingOfficeCustoms, TransportationOffices.CounselingOffice, TransportationOffice) + transportationOffice = BuildTransportationOffice(db, tempCounselingOfficeCustoms, nil) + } + + if closeoutOfficeResult == nil && counselingOfficeResult == nil { + transportationOffice = BuildTransportationOffice(db, customs, nil) + } // create officeuser officeUser := models.OfficeUser{ @@ -51,6 +68,7 @@ func BuildOfficeUser(db *pop.Connection, customs []Customization, traits []Trait TransportationOffice: transportationOffice, TransportationOfficeID: transportationOffice.ID, } + // Overwrite values with those from assertions testdatagen.MergeModels(&officeUser, cOfficeUser) @@ -200,7 +218,25 @@ func BuildOfficeUserWithPrivileges(db *pop.Connection, customs []Customization, user := BuildUserAndUsersRolesAndUsersPrivileges(db, customs, nil) // Find/create the TransportationOffice model - transportationOffice := BuildTransportationOffice(db, customs, nil) + var transportationOffice models.TransportationOffice + + tempCloseoutOfficeCustoms := customs + closeoutOfficeResult := findValidCustomization(customs, TransportationOffices.CloseoutOffice) + if closeoutOfficeResult != nil { + tempCloseoutOfficeCustoms = convertCustomizationInList(tempCloseoutOfficeCustoms, TransportationOffices.CloseoutOffice, TransportationOffice) + transportationOffice = BuildTransportationOffice(db, tempCloseoutOfficeCustoms, nil) + } + + tempCounselingOfficeCustoms := customs + counselingOfficeResult := findValidCustomization(customs, TransportationOffices.CounselingOffice) + if counselingOfficeResult != nil { + tempCounselingOfficeCustoms = convertCustomizationInList(tempCounselingOfficeCustoms, TransportationOffices.CounselingOffice, TransportationOffice) + transportationOffice = BuildTransportationOffice(db, tempCounselingOfficeCustoms, nil) + } + + if closeoutOfficeResult == nil && counselingOfficeResult == nil { + transportationOffice = BuildTransportationOffice(db, customs, nil) + } // create officeuser officeUser := models.OfficeUser{ @@ -213,6 +249,7 @@ func BuildOfficeUserWithPrivileges(db *pop.Connection, customs []Customization, TransportationOffice: transportationOffice, TransportationOfficeID: transportationOffice.ID, } + // Overwrite values with those from assertions testdatagen.MergeModels(&officeUser, cOfficeUser) diff --git a/pkg/factory/office_user_factory_test.go b/pkg/factory/office_user_factory_test.go index ebcbb56a3b2..31540b49662 100644 --- a/pkg/factory/office_user_factory_test.go +++ b/pkg/factory/office_user_factory_test.go @@ -303,4 +303,54 @@ func (suite *FactorySuite) TestBuildOfficeUserExtra() { suite.NoError(err) suite.Equal(precountUsersRoles, countUsersRoles) }) + + suite.Run("Successful creation of OfficeUser using BuildOfficeUserWithPrivileges with customizations", func() { + email := "example@mail.com" + transportationOffice := BuildTransportationOffice(suite.DB(), []Customization{ + { + Model: models.TransportationOffice{ + ProvidesCloseout: true, + }, + }, + }, nil) + officeUser := BuildOfficeUserWithPrivileges(suite.DB(), []Customization{ + { + Model: models.OfficeUser{ + FirstName: "Riley", + Email: email, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &TransportationOffices.CloseoutOffice, + }, + { + Model: models.User{ + OktaEmail: email, + Privileges: []models.Privilege{ + { + PrivilegeType: models.PrivilegeTypeSupervisor, + }, + }, + Roles: []roles.Role{ + { + RoleType: roles.RoleTypeServicesCounselor, + }, + }, + }, + }, + }, nil) + + // Check that the user has the office user role + _, hasRole := officeUser.User.Roles.GetRole(roles.RoleTypeServicesCounselor) + _, hasPrivilege := officeUser.User.Privileges.GetPrivilege(models.PrivilegeTypeSupervisor) + suite.True(hasRole) + suite.True(hasPrivilege) + // Check customizations + suite.Equal(email, officeUser.User.OktaEmail) + suite.Equal(email, officeUser.Email) + suite.Equal("Riley", officeUser.FirstName) + suite.Equal(transportationOffice.ID, officeUser.TransportationOfficeID) + }) } diff --git a/pkg/factory/shared.go b/pkg/factory/shared.go index bc49aed27ff..b4e376f5425 100644 --- a/pkg/factory/shared.go +++ b/pkg/factory/shared.go @@ -258,6 +258,7 @@ type transportationOfficeGroup struct { OriginDutyLocation CustomType NewDutyLocation CustomType CloseoutOffice CustomType + CounselingOffice CustomType } // TransportationOffices is the struct to access the fields externally @@ -265,6 +266,15 @@ var TransportationOffices = transportationOfficeGroup{ OriginDutyLocation: "OriginDutyLocationTransportationOffice", NewDutyLocation: "NewDutyLocationTransportationOffice", CloseoutOffice: "CloseoutOffice", + CounselingOffice: "CounselingOffice", +} + +type officeUserGroup struct { + SCAssignedUser CustomType +} + +var OfficeUsers = officeUserGroup{ + SCAssignedUser: "SCAssignedUser", } // uploadGroup is a grouping of all the upload related fields diff --git a/pkg/gen/ghcapi/configure_mymove.go b/pkg/gen/ghcapi/configure_mymove.go index c4c3d349ed6..2c7449ec836 100644 --- a/pkg/gen/ghcapi/configure_mymove.go +++ b/pkg/gen/ghcapi/configure_mymove.go @@ -222,6 +222,11 @@ func configureAPI(api *ghcoperations.MymoveAPI) http.Handler { return middleware.NotImplemented("operation re_service_items.GetAllReServiceItems has not yet been implemented") }) } + if api.QueuesGetBulkAssignmentDataHandler == nil { + api.QueuesGetBulkAssignmentDataHandler = queues.GetBulkAssignmentDataHandlerFunc(func(params queues.GetBulkAssignmentDataParams) middleware.Responder { + return middleware.NotImplemented("operation queues.GetBulkAssignmentData has not yet been implemented") + }) + } if api.CustomerGetCustomerHandler == nil { api.CustomerGetCustomerHandler = customer.GetCustomerHandlerFunc(func(params customer.GetCustomerParams) middleware.Responder { return middleware.NotImplemented("operation customer.GetCustomer has not yet been implemented") diff --git a/pkg/gen/ghcapi/embedded_spec.go b/pkg/gen/ghcapi/embedded_spec.go index 361f666c222..a95f64946e8 100644 --- a/pkg/gen/ghcapi/embedded_spec.go +++ b/pkg/gen/ghcapi/embedded_spec.go @@ -4312,6 +4312,50 @@ func init() { } } }, + "/queues/bulk-assignment": { + "get": { + "description": "Supervisor office users are able to bulk assign moves. This endpoint returns the relevant data to them; the current workload of the office users that work under them, and the moves that are available to be assigned\n", + "produces": [ + "application/json" + ], + "tags": [ + "queues" + ], + "summary": "Gets data for bulk assignment modal", + "operationId": "getBulkAssignmentData", + "parameters": [ + { + "enum": [ + "COUNSELING", + "CLOSEOUT", + "TASK_ORDER", + "PAYMENT_REQUEST" + ], + "type": "string", + "description": "A string corresponding to the queue type", + "name": "queueType", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Successfully returned bulk assignment data", + "schema": { + "$ref": "#/definitions/BulkAssignmentData" + } + }, + "401": { + "$ref": "#/responses/PermissionDenied" + }, + "404": { + "$ref": "#/responses/NotFound" + }, + "500": { + "$ref": "#/responses/ServerError" + } + } + } + }, "/queues/counseling": { "get": { "description": "An office services counselor user will be assigned a transportation office that will determine which moves are displayed in their queue based on the origin duty location. GHC moves will show up here onced they have reached the NEEDS SERVICE COUNSELING status after submission from a customer or created on a customer's behalf.\n", @@ -6687,6 +6731,9 @@ func init() { "firstName": { "type": "string" }, + "hasSafetyPrivilege": { + "type": "boolean" + }, "lastName": { "type": "string" }, @@ -6694,6 +6741,9 @@ func init() { "type": "string", "format": "uuid", "example": "c56a4180-65aa-42ec-a945-5fd21dec0538" + }, + "workload": { + "type": "integer" } } }, @@ -6816,6 +6866,28 @@ func init() { }, "x-nullable": true }, + "BulkAssignmentData": { + "type": "object", + "properties": { + "availableOfficeUsers": { + "$ref": "#/definitions/AvailableOfficeUsers" + }, + "bulkAssignmentMoveIDs": { + "$ref": "#/definitions/BulkAssignmentMoveIDs" + } + } + }, + "BulkAssignmentMoveID": { + "type": "string", + "format": "uuid", + "example": "c56a4180-65aa-42ec-a945-5fd21dec0538" + }, + "BulkAssignmentMoveIDs": { + "type": "array", + "items": { + "$ref": "#/definitions/BulkAssignmentMoveID" + } + }, "ClientError": { "type": "object", "required": [ @@ -20658,6 +20730,59 @@ func init() { } } }, + "/queues/bulk-assignment": { + "get": { + "description": "Supervisor office users are able to bulk assign moves. This endpoint returns the relevant data to them; the current workload of the office users that work under them, and the moves that are available to be assigned\n", + "produces": [ + "application/json" + ], + "tags": [ + "queues" + ], + "summary": "Gets data for bulk assignment modal", + "operationId": "getBulkAssignmentData", + "parameters": [ + { + "enum": [ + "COUNSELING", + "CLOSEOUT", + "TASK_ORDER", + "PAYMENT_REQUEST" + ], + "type": "string", + "description": "A string corresponding to the queue type", + "name": "queueType", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Successfully returned bulk assignment data", + "schema": { + "$ref": "#/definitions/BulkAssignmentData" + } + }, + "401": { + "description": "The request was denied", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "The requested resource wasn't found", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "500": { + "description": "A server error occurred", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, "/queues/counseling": { "get": { "description": "An office services counselor user will be assigned a transportation office that will determine which moves are displayed in their queue based on the origin duty location. GHC moves will show up here onced they have reached the NEEDS SERVICE COUNSELING status after submission from a customer or created on a customer's behalf.\n", @@ -23427,6 +23552,9 @@ func init() { "firstName": { "type": "string" }, + "hasSafetyPrivilege": { + "type": "boolean" + }, "lastName": { "type": "string" }, @@ -23434,6 +23562,9 @@ func init() { "type": "string", "format": "uuid", "example": "c56a4180-65aa-42ec-a945-5fd21dec0538" + }, + "workload": { + "type": "integer" } } }, @@ -23556,6 +23687,28 @@ func init() { }, "x-nullable": true }, + "BulkAssignmentData": { + "type": "object", + "properties": { + "availableOfficeUsers": { + "$ref": "#/definitions/AvailableOfficeUsers" + }, + "bulkAssignmentMoveIDs": { + "$ref": "#/definitions/BulkAssignmentMoveIDs" + } + } + }, + "BulkAssignmentMoveID": { + "type": "string", + "format": "uuid", + "example": "c56a4180-65aa-42ec-a945-5fd21dec0538" + }, + "BulkAssignmentMoveIDs": { + "type": "array", + "items": { + "$ref": "#/definitions/BulkAssignmentMoveID" + } + }, "ClientError": { "type": "object", "required": [ diff --git a/pkg/gen/ghcapi/ghcoperations/mymove_api.go b/pkg/gen/ghcapi/ghcoperations/mymove_api.go index 10a373d0be4..e64a6704256 100644 --- a/pkg/gen/ghcapi/ghcoperations/mymove_api.go +++ b/pkg/gen/ghcapi/ghcoperations/mymove_api.go @@ -161,6 +161,9 @@ func NewMymoveAPI(spec *loads.Document) *MymoveAPI { ReServiceItemsGetAllReServiceItemsHandler: re_service_items.GetAllReServiceItemsHandlerFunc(func(params re_service_items.GetAllReServiceItemsParams) middleware.Responder { return middleware.NotImplemented("operation re_service_items.GetAllReServiceItems has not yet been implemented") }), + QueuesGetBulkAssignmentDataHandler: queues.GetBulkAssignmentDataHandlerFunc(func(params queues.GetBulkAssignmentDataParams) middleware.Responder { + return middleware.NotImplemented("operation queues.GetBulkAssignmentData has not yet been implemented") + }), CustomerGetCustomerHandler: customer.GetCustomerHandlerFunc(func(params customer.GetCustomerParams) middleware.Responder { return middleware.NotImplemented("operation customer.GetCustomer has not yet been implemented") }), @@ -495,6 +498,8 @@ type MymoveAPI struct { PpmFinishDocumentReviewHandler ppm.FinishDocumentReviewHandler // ReServiceItemsGetAllReServiceItemsHandler sets the operation handler for the get all re service items operation ReServiceItemsGetAllReServiceItemsHandler re_service_items.GetAllReServiceItemsHandler + // QueuesGetBulkAssignmentDataHandler sets the operation handler for the get bulk assignment data operation + QueuesGetBulkAssignmentDataHandler queues.GetBulkAssignmentDataHandler // CustomerGetCustomerHandler sets the operation handler for the get customer operation CustomerGetCustomerHandler customer.GetCustomerHandler // CustomerSupportRemarksGetCustomerSupportRemarksForMoveHandler sets the operation handler for the get customer support remarks for move operation @@ -820,6 +825,9 @@ func (o *MymoveAPI) Validate() error { if o.ReServiceItemsGetAllReServiceItemsHandler == nil { unregistered = append(unregistered, "re_service_items.GetAllReServiceItemsHandler") } + if o.QueuesGetBulkAssignmentDataHandler == nil { + unregistered = append(unregistered, "queues.GetBulkAssignmentDataHandler") + } if o.CustomerGetCustomerHandler == nil { unregistered = append(unregistered, "customer.GetCustomerHandler") } @@ -1263,6 +1271,10 @@ func (o *MymoveAPI) initHandlerCache() { if o.handlers["GET"] == nil { o.handlers["GET"] = make(map[string]http.Handler) } + o.handlers["GET"]["/queues/bulk-assignment"] = queues.NewGetBulkAssignmentData(o.context, o.QueuesGetBulkAssignmentDataHandler) + if o.handlers["GET"] == nil { + o.handlers["GET"] = make(map[string]http.Handler) + } o.handlers["GET"]["/customer/{customerID}"] = customer.NewGetCustomer(o.context, o.CustomerGetCustomerHandler) if o.handlers["GET"] == nil { o.handlers["GET"] = make(map[string]http.Handler) diff --git a/pkg/gen/ghcapi/ghcoperations/queues/get_bulk_assignment_data.go b/pkg/gen/ghcapi/ghcoperations/queues/get_bulk_assignment_data.go new file mode 100644 index 00000000000..a48fbcedc45 --- /dev/null +++ b/pkg/gen/ghcapi/ghcoperations/queues/get_bulk_assignment_data.go @@ -0,0 +1,58 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package queues + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime/middleware" +) + +// GetBulkAssignmentDataHandlerFunc turns a function with the right signature into a get bulk assignment data handler +type GetBulkAssignmentDataHandlerFunc func(GetBulkAssignmentDataParams) middleware.Responder + +// Handle executing the request and returning a response +func (fn GetBulkAssignmentDataHandlerFunc) Handle(params GetBulkAssignmentDataParams) middleware.Responder { + return fn(params) +} + +// GetBulkAssignmentDataHandler interface for that can handle valid get bulk assignment data params +type GetBulkAssignmentDataHandler interface { + Handle(GetBulkAssignmentDataParams) middleware.Responder +} + +// NewGetBulkAssignmentData creates a new http.Handler for the get bulk assignment data operation +func NewGetBulkAssignmentData(ctx *middleware.Context, handler GetBulkAssignmentDataHandler) *GetBulkAssignmentData { + return &GetBulkAssignmentData{Context: ctx, Handler: handler} +} + +/* + GetBulkAssignmentData swagger:route GET /queues/bulk-assignment queues getBulkAssignmentData + +# Gets data for bulk assignment modal + +Supervisor office users are able to bulk assign moves. This endpoint returns the relevant data to them; the current workload of the office users that work under them, and the moves that are available to be assigned +*/ +type GetBulkAssignmentData struct { + Context *middleware.Context + Handler GetBulkAssignmentDataHandler +} + +func (o *GetBulkAssignmentData) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + route, rCtx, _ := o.Context.RouteInfo(r) + if rCtx != nil { + *r = *rCtx + } + var Params = NewGetBulkAssignmentDataParams() + if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params + o.Context.Respond(rw, r, route.Produces, route, err) + return + } + + res := o.Handler.Handle(Params) // actually handle the request + o.Context.Respond(rw, r, route.Produces, route, res) + +} diff --git a/pkg/gen/ghcapi/ghcoperations/queues/get_bulk_assignment_data_parameters.go b/pkg/gen/ghcapi/ghcoperations/queues/get_bulk_assignment_data_parameters.go new file mode 100644 index 00000000000..84675ac3ec4 --- /dev/null +++ b/pkg/gen/ghcapi/ghcoperations/queues/get_bulk_assignment_data_parameters.go @@ -0,0 +1,92 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package queues + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + "github.com/go-openapi/runtime/middleware" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/validate" +) + +// NewGetBulkAssignmentDataParams creates a new GetBulkAssignmentDataParams object +// +// There are no default values defined in the spec. +func NewGetBulkAssignmentDataParams() GetBulkAssignmentDataParams { + + return GetBulkAssignmentDataParams{} +} + +// GetBulkAssignmentDataParams contains all the bound params for the get bulk assignment data operation +// typically these are obtained from a http.Request +// +// swagger:parameters getBulkAssignmentData +type GetBulkAssignmentDataParams struct { + + // HTTP Request Object + HTTPRequest *http.Request `json:"-"` + + /*A string corresponding to the queue type + In: query + */ + QueueType *string +} + +// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface +// for simple values it will use straight method calls. +// +// To ensure default values, the struct must have been initialized with NewGetBulkAssignmentDataParams() beforehand. +func (o *GetBulkAssignmentDataParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error { + var res []error + + o.HTTPRequest = r + + qs := runtime.Values(r.URL.Query()) + + qQueueType, qhkQueueType, _ := qs.GetOK("queueType") + if err := o.bindQueueType(qQueueType, qhkQueueType, route.Formats); err != nil { + res = append(res, err) + } + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +// bindQueueType binds and validates parameter QueueType from query. +func (o *GetBulkAssignmentDataParams) bindQueueType(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: false + // AllowEmptyValue: false + + if raw == "" { // empty values pass all other validations + return nil + } + o.QueueType = &raw + + if err := o.validateQueueType(formats); err != nil { + return err + } + + return nil +} + +// validateQueueType carries on validations for parameter QueueType +func (o *GetBulkAssignmentDataParams) validateQueueType(formats strfmt.Registry) error { + + if err := validate.EnumCase("queueType", "query", *o.QueueType, []interface{}{"COUNSELING", "CLOSEOUT", "TASK_ORDER", "PAYMENT_REQUEST"}, true); err != nil { + return err + } + + return nil +} diff --git a/pkg/gen/ghcapi/ghcoperations/queues/get_bulk_assignment_data_responses.go b/pkg/gen/ghcapi/ghcoperations/queues/get_bulk_assignment_data_responses.go new file mode 100644 index 00000000000..7aff474a411 --- /dev/null +++ b/pkg/gen/ghcapi/ghcoperations/queues/get_bulk_assignment_data_responses.go @@ -0,0 +1,194 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package queues + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime" + + "github.com/transcom/mymove/pkg/gen/ghcmessages" +) + +// GetBulkAssignmentDataOKCode is the HTTP code returned for type GetBulkAssignmentDataOK +const GetBulkAssignmentDataOKCode int = 200 + +/* +GetBulkAssignmentDataOK Successfully returned bulk assignment data + +swagger:response getBulkAssignmentDataOK +*/ +type GetBulkAssignmentDataOK struct { + + /* + In: Body + */ + Payload *ghcmessages.BulkAssignmentData `json:"body,omitempty"` +} + +// NewGetBulkAssignmentDataOK creates GetBulkAssignmentDataOK with default headers values +func NewGetBulkAssignmentDataOK() *GetBulkAssignmentDataOK { + + return &GetBulkAssignmentDataOK{} +} + +// WithPayload adds the payload to the get bulk assignment data o k response +func (o *GetBulkAssignmentDataOK) WithPayload(payload *ghcmessages.BulkAssignmentData) *GetBulkAssignmentDataOK { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the get bulk assignment data o k response +func (o *GetBulkAssignmentDataOK) SetPayload(payload *ghcmessages.BulkAssignmentData) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *GetBulkAssignmentDataOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(200) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +// GetBulkAssignmentDataUnauthorizedCode is the HTTP code returned for type GetBulkAssignmentDataUnauthorized +const GetBulkAssignmentDataUnauthorizedCode int = 401 + +/* +GetBulkAssignmentDataUnauthorized The request was denied + +swagger:response getBulkAssignmentDataUnauthorized +*/ +type GetBulkAssignmentDataUnauthorized struct { + + /* + In: Body + */ + Payload *ghcmessages.Error `json:"body,omitempty"` +} + +// NewGetBulkAssignmentDataUnauthorized creates GetBulkAssignmentDataUnauthorized with default headers values +func NewGetBulkAssignmentDataUnauthorized() *GetBulkAssignmentDataUnauthorized { + + return &GetBulkAssignmentDataUnauthorized{} +} + +// WithPayload adds the payload to the get bulk assignment data unauthorized response +func (o *GetBulkAssignmentDataUnauthorized) WithPayload(payload *ghcmessages.Error) *GetBulkAssignmentDataUnauthorized { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the get bulk assignment data unauthorized response +func (o *GetBulkAssignmentDataUnauthorized) SetPayload(payload *ghcmessages.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *GetBulkAssignmentDataUnauthorized) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(401) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +// GetBulkAssignmentDataNotFoundCode is the HTTP code returned for type GetBulkAssignmentDataNotFound +const GetBulkAssignmentDataNotFoundCode int = 404 + +/* +GetBulkAssignmentDataNotFound The requested resource wasn't found + +swagger:response getBulkAssignmentDataNotFound +*/ +type GetBulkAssignmentDataNotFound struct { + + /* + In: Body + */ + Payload *ghcmessages.Error `json:"body,omitempty"` +} + +// NewGetBulkAssignmentDataNotFound creates GetBulkAssignmentDataNotFound with default headers values +func NewGetBulkAssignmentDataNotFound() *GetBulkAssignmentDataNotFound { + + return &GetBulkAssignmentDataNotFound{} +} + +// WithPayload adds the payload to the get bulk assignment data not found response +func (o *GetBulkAssignmentDataNotFound) WithPayload(payload *ghcmessages.Error) *GetBulkAssignmentDataNotFound { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the get bulk assignment data not found response +func (o *GetBulkAssignmentDataNotFound) SetPayload(payload *ghcmessages.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *GetBulkAssignmentDataNotFound) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(404) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +// GetBulkAssignmentDataInternalServerErrorCode is the HTTP code returned for type GetBulkAssignmentDataInternalServerError +const GetBulkAssignmentDataInternalServerErrorCode int = 500 + +/* +GetBulkAssignmentDataInternalServerError A server error occurred + +swagger:response getBulkAssignmentDataInternalServerError +*/ +type GetBulkAssignmentDataInternalServerError struct { + + /* + In: Body + */ + Payload *ghcmessages.Error `json:"body,omitempty"` +} + +// NewGetBulkAssignmentDataInternalServerError creates GetBulkAssignmentDataInternalServerError with default headers values +func NewGetBulkAssignmentDataInternalServerError() *GetBulkAssignmentDataInternalServerError { + + return &GetBulkAssignmentDataInternalServerError{} +} + +// WithPayload adds the payload to the get bulk assignment data internal server error response +func (o *GetBulkAssignmentDataInternalServerError) WithPayload(payload *ghcmessages.Error) *GetBulkAssignmentDataInternalServerError { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the get bulk assignment data internal server error response +func (o *GetBulkAssignmentDataInternalServerError) SetPayload(payload *ghcmessages.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *GetBulkAssignmentDataInternalServerError) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(500) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} diff --git a/pkg/gen/ghcapi/ghcoperations/queues/get_bulk_assignment_data_urlbuilder.go b/pkg/gen/ghcapi/ghcoperations/queues/get_bulk_assignment_data_urlbuilder.go new file mode 100644 index 00000000000..eda687319c9 --- /dev/null +++ b/pkg/gen/ghcapi/ghcoperations/queues/get_bulk_assignment_data_urlbuilder.go @@ -0,0 +1,103 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package queues + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "errors" + "net/url" + golangswaggerpaths "path" +) + +// GetBulkAssignmentDataURL generates an URL for the get bulk assignment data operation +type GetBulkAssignmentDataURL struct { + QueueType *string + + _basePath string + // avoid unkeyed usage + _ struct{} +} + +// WithBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *GetBulkAssignmentDataURL) WithBasePath(bp string) *GetBulkAssignmentDataURL { + o.SetBasePath(bp) + return o +} + +// SetBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *GetBulkAssignmentDataURL) SetBasePath(bp string) { + o._basePath = bp +} + +// Build a url path and query string +func (o *GetBulkAssignmentDataURL) Build() (*url.URL, error) { + var _result url.URL + + var _path = "/queues/bulk-assignment" + + _basePath := o._basePath + if _basePath == "" { + _basePath = "/ghc/v1" + } + _result.Path = golangswaggerpaths.Join(_basePath, _path) + + qs := make(url.Values) + + var queueTypeQ string + if o.QueueType != nil { + queueTypeQ = *o.QueueType + } + if queueTypeQ != "" { + qs.Set("queueType", queueTypeQ) + } + + _result.RawQuery = qs.Encode() + + return &_result, nil +} + +// Must is a helper function to panic when the url builder returns an error +func (o *GetBulkAssignmentDataURL) Must(u *url.URL, err error) *url.URL { + if err != nil { + panic(err) + } + if u == nil { + panic("url can't be nil") + } + return u +} + +// String returns the string representation of the path with query string +func (o *GetBulkAssignmentDataURL) String() string { + return o.Must(o.Build()).String() +} + +// BuildFull builds a full url with scheme, host, path and query string +func (o *GetBulkAssignmentDataURL) BuildFull(scheme, host string) (*url.URL, error) { + if scheme == "" { + return nil, errors.New("scheme is required for a full url on GetBulkAssignmentDataURL") + } + if host == "" { + return nil, errors.New("host is required for a full url on GetBulkAssignmentDataURL") + } + + base, err := o.Build() + if err != nil { + return nil, err + } + + base.Scheme = scheme + base.Host = host + return base, nil +} + +// StringFull returns the string representation of a complete url +func (o *GetBulkAssignmentDataURL) StringFull(scheme, host string) string { + return o.Must(o.BuildFull(scheme, host)).String() +} diff --git a/pkg/gen/ghcmessages/available_office_user.go b/pkg/gen/ghcmessages/available_office_user.go index 5023b2ea1dd..bb9b84b48dc 100644 --- a/pkg/gen/ghcmessages/available_office_user.go +++ b/pkg/gen/ghcmessages/available_office_user.go @@ -22,6 +22,9 @@ type AvailableOfficeUser struct { // first name FirstName string `json:"firstName,omitempty"` + // has safety privilege + HasSafetyPrivilege bool `json:"hasSafetyPrivilege,omitempty"` + // last name LastName string `json:"lastName,omitempty"` @@ -29,6 +32,9 @@ type AvailableOfficeUser struct { // Example: c56a4180-65aa-42ec-a945-5fd21dec0538 // Format: uuid OfficeUserID strfmt.UUID `json:"officeUserId,omitempty"` + + // workload + Workload int64 `json:"workload,omitempty"` } // Validate validates this available office user diff --git a/pkg/gen/ghcmessages/bulk_assignment_data.go b/pkg/gen/ghcmessages/bulk_assignment_data.go new file mode 100644 index 00000000000..c37e32819a9 --- /dev/null +++ b/pkg/gen/ghcmessages/bulk_assignment_data.go @@ -0,0 +1,142 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package ghcmessages + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" +) + +// BulkAssignmentData bulk assignment data +// +// swagger:model BulkAssignmentData +type BulkAssignmentData struct { + + // available office users + AvailableOfficeUsers AvailableOfficeUsers `json:"availableOfficeUsers,omitempty"` + + // bulk assignment move i ds + BulkAssignmentMoveIDs BulkAssignmentMoveIDs `json:"bulkAssignmentMoveIDs,omitempty"` +} + +// Validate validates this bulk assignment data +func (m *BulkAssignmentData) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateAvailableOfficeUsers(formats); err != nil { + res = append(res, err) + } + + if err := m.validateBulkAssignmentMoveIDs(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *BulkAssignmentData) validateAvailableOfficeUsers(formats strfmt.Registry) error { + if swag.IsZero(m.AvailableOfficeUsers) { // not required + return nil + } + + if err := m.AvailableOfficeUsers.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("availableOfficeUsers") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("availableOfficeUsers") + } + return err + } + + return nil +} + +func (m *BulkAssignmentData) validateBulkAssignmentMoveIDs(formats strfmt.Registry) error { + if swag.IsZero(m.BulkAssignmentMoveIDs) { // not required + return nil + } + + if err := m.BulkAssignmentMoveIDs.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("bulkAssignmentMoveIDs") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("bulkAssignmentMoveIDs") + } + return err + } + + return nil +} + +// ContextValidate validate this bulk assignment data based on the context it is used +func (m *BulkAssignmentData) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateAvailableOfficeUsers(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateBulkAssignmentMoveIDs(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *BulkAssignmentData) contextValidateAvailableOfficeUsers(ctx context.Context, formats strfmt.Registry) error { + + if err := m.AvailableOfficeUsers.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("availableOfficeUsers") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("availableOfficeUsers") + } + return err + } + + return nil +} + +func (m *BulkAssignmentData) contextValidateBulkAssignmentMoveIDs(ctx context.Context, formats strfmt.Registry) error { + + if err := m.BulkAssignmentMoveIDs.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("bulkAssignmentMoveIDs") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("bulkAssignmentMoveIDs") + } + return err + } + + return nil +} + +// MarshalBinary interface implementation +func (m *BulkAssignmentData) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *BulkAssignmentData) UnmarshalBinary(b []byte) error { + var res BulkAssignmentData + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/pkg/gen/ghcmessages/bulk_assignment_move_i_ds.go b/pkg/gen/ghcmessages/bulk_assignment_move_i_ds.go new file mode 100644 index 00000000000..5b34ee2a4bc --- /dev/null +++ b/pkg/gen/ghcmessages/bulk_assignment_move_i_ds.go @@ -0,0 +1,70 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package ghcmessages + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "strconv" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" +) + +// BulkAssignmentMoveIDs bulk assignment move i ds +// +// swagger:model BulkAssignmentMoveIDs +type BulkAssignmentMoveIDs []BulkAssignmentMoveID + +// Validate validates this bulk assignment move i ds +func (m BulkAssignmentMoveIDs) Validate(formats strfmt.Registry) error { + var res []error + + for i := 0; i < len(m); i++ { + + if err := m[i].Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName(strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName(strconv.Itoa(i)) + } + return err + } + + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +// ContextValidate validate this bulk assignment move i ds based on the context it is used +func (m BulkAssignmentMoveIDs) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + for i := 0; i < len(m); i++ { + + if swag.IsZero(m[i]) { // not required + return nil + } + + if err := m[i].ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName(strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName(strconv.Itoa(i)) + } + return err + } + + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/pkg/gen/ghcmessages/bulk_assignment_move_id.go b/pkg/gen/ghcmessages/bulk_assignment_move_id.go new file mode 100644 index 00000000000..505c9541f63 --- /dev/null +++ b/pkg/gen/ghcmessages/bulk_assignment_move_id.go @@ -0,0 +1,39 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package ghcmessages + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/validate" +) + +// BulkAssignmentMoveID bulk assignment move ID +// Example: c56a4180-65aa-42ec-a945-5fd21dec0538 +// +// swagger:model BulkAssignmentMoveID +type BulkAssignmentMoveID strfmt.UUID + +// Validate validates this bulk assignment move ID +func (m BulkAssignmentMoveID) Validate(formats strfmt.Registry) error { + var res []error + + if err := validate.FormatOf("", "body", "uuid", strfmt.UUID(m).String(), formats); err != nil { + return err + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +// ContextValidate validates this bulk assignment move ID based on context it is used +func (m BulkAssignmentMoveID) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} diff --git a/pkg/handlers/ghcapi/api.go b/pkg/handlers/ghcapi/api.go index 1e6d7fcb11c..1b0a66d1956 100644 --- a/pkg/handlers/ghcapi/api.go +++ b/pkg/handlers/ghcapi/api.go @@ -530,6 +530,12 @@ func NewGhcAPIHandler(handlerConfig handlers.HandlerConfig) *ghcops.MymoveAPI { ghcAPI.GhcDocumentsGetDocumentHandler = GetDocumentHandler{handlerConfig} ghcAPI.GhcDocumentsCreateDocumentHandler = CreateDocumentHandler{handlerConfig} + ghcAPI.QueuesGetBulkAssignmentDataHandler = GetBulkAssignmentDataHandler{ + handlerConfig, + officeusercreator.NewOfficeUserFetcherPop(), + move.NewMoveFetcherBulkAssignment(), + } + ghcAPI.QueuesGetMovesQueueHandler = GetMovesQueueHandler{ handlerConfig, order.NewOrderFetcher(), diff --git a/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go b/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go index caa4aef11d5..db8c54617ed 100644 --- a/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go +++ b/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go @@ -2159,6 +2159,30 @@ func QueueAvailableOfficeUsers(officeUsers []models.OfficeUser) *ghcmessages.Ava return &availableOfficeUsers } +func BulkAssignmentData(appCtx appcontext.AppContext, moves []models.MoveWithEarliestDate, officeUsers []models.OfficeUserWithWorkload, officeId uuid.UUID) ghcmessages.BulkAssignmentData { + availableOfficeUsers := make(ghcmessages.AvailableOfficeUsers, len(officeUsers)) + availableMoves := make(ghcmessages.BulkAssignmentMoveIDs, len(moves)) + + for i, officeUser := range officeUsers { + availableOfficeUsers[i] = &ghcmessages.AvailableOfficeUser{ + LastName: officeUser.LastName, + FirstName: officeUser.FirstName, + OfficeUserID: *handlers.FmtUUID(officeUser.ID), + Workload: int64(officeUser.Workload), + } + } + for i, move := range moves { + availableMoves[i] = ghcmessages.BulkAssignmentMoveID(strfmt.UUID(move.ID.String())) + } + + bulkAssignmentData := &ghcmessages.BulkAssignmentData{ + AvailableOfficeUsers: availableOfficeUsers, + BulkAssignmentMoveIDs: availableMoves, + } + + return *bulkAssignmentData +} + func queueMoveIsAssignable(move models.Move, assignedToUser *ghcmessages.AssignedOfficeUser, isCloseoutQueue bool, officeUser models.OfficeUser, ppmCloseoutGblocs bool) bool { // default to false isAssignable := false diff --git a/pkg/handlers/ghcapi/queues.go b/pkg/handlers/ghcapi/queues.go index 5ffbedb35d5..ed4980df650 100644 --- a/pkg/handlers/ghcapi/queues.go +++ b/pkg/handlers/ghcapi/queues.go @@ -551,6 +551,93 @@ func (h GetServicesCounselingQueueHandler) Handle( }) } +// GetBulkAssignmentDataHandler returns moves that the supervisor can assign, along with the office users they are able to assign to +type GetBulkAssignmentDataHandler struct { + handlers.HandlerConfig + services.OfficeUserFetcherPop + services.MoveFetcherBulkAssignment +} + +func (h GetBulkAssignmentDataHandler) Handle( + params queues.GetBulkAssignmentDataParams, +) middleware.Responder { + return h.AuditableAppContextFromRequestWithErrors(params.HTTPRequest, + func(appCtx appcontext.AppContext) (middleware.Responder, error) { + if !appCtx.Session().IsOfficeUser() { + err := apperror.NewForbiddenError("not an office user") + appCtx.Logger().Error("Must be an office user", zap.Error(err)) + return queues.NewGetBulkAssignmentDataUnauthorized(), err + } + + officeUser, err := h.OfficeUserFetcherPop.FetchOfficeUserByID(appCtx, appCtx.Session().OfficeUserID) + if err != nil { + appCtx.Logger().Error("Error retrieving office_user", zap.Error(err)) + return queues.NewGetBulkAssignmentDataNotFound(), err + } + + privileges, err := models.FetchPrivilegesForUser(appCtx.DB(), *officeUser.UserID) + if err != nil { + appCtx.Logger().Error("Error retreiving user privileges", zap.Error(err)) + return queues.NewGetBulkAssignmentDataNotFound(), err + } + + isSupervisor := privileges.HasPrivilege(models.PrivilegeTypeSupervisor) + if !isSupervisor { + appCtx.Logger().Error("Unauthorized", zap.Error(err)) + return queues.NewGetBulkAssignmentDataUnauthorized(), err + } + + queueType := params.QueueType + var officeUserData ghcmessages.BulkAssignmentData + + switch *queueType { + case string(models.QueueTypeCounseling): + // fetch the Services Counselors who work at their office + officeUsers, err := h.OfficeUserFetcherPop.FetchOfficeUsersWithWorkloadByRoleAndOffice( + appCtx, + roles.RoleTypeServicesCounselor, + officeUser.TransportationOfficeID, + ) + if err != nil { + appCtx.Logger().Error("Error retreiving office users", zap.Error(err)) + return queues.NewGetBulkAssignmentDataInternalServerError(), err + } + // fetch the moves available to be assigned to their office users + moves, err := h.MoveFetcherBulkAssignment.FetchMovesForBulkAssignmentCounseling( + appCtx, officeUser.TransportationOffice.Gbloc, officeUser.TransportationOffice.ID, + ) + if err != nil { + appCtx.Logger().Error("Error retreiving moves", zap.Error(err)) + return queues.NewGetBulkAssignmentDataInternalServerError(), err + } + + officeUserData = payloads.BulkAssignmentData(appCtx, moves, officeUsers, officeUser.TransportationOffice.ID) + case string(models.QueueTypeTaskOrder): + // fetch the TOOs who work at their office + officeUsers, err := h.OfficeUserFetcherPop.FetchOfficeUsersWithWorkloadByRoleAndOffice( + appCtx, + roles.RoleTypeTOO, + officeUser.TransportationOfficeID, + ) + if err != nil { + appCtx.Logger().Error("Error retreiving office users", zap.Error(err)) + return queues.NewGetBulkAssignmentDataInternalServerError(), err + } + // fetch the moves available to be assigned to their office users + moves, err := h.MoveFetcherBulkAssignment.FetchMovesForBulkAssignmentTaskOrder( + appCtx, officeUser.TransportationOffice.Gbloc, officeUser.TransportationOffice.ID, + ) + if err != nil { + appCtx.Logger().Error("Error retreiving moves", zap.Error(err)) + return queues.NewGetBulkAssignmentDataInternalServerError(), err + } + + officeUserData = payloads.BulkAssignmentData(appCtx, moves, officeUsers, officeUser.TransportationOffice.ID) + } + return queues.NewGetBulkAssignmentDataOK().WithPayload(&officeUserData), nil + }) +} + // GetServicesCounselingOriginListHandler returns the origin list for the Service Counselor user via GET /queues/counselor/origin-list type GetServicesCounselingOriginListHandler struct { handlers.HandlerConfig @@ -576,7 +663,6 @@ func (h GetServicesCounselingOriginListHandler) Handle( ListOrderParams := services.ListOrderParams{ NeedsPPMCloseout: params.NeedsPPMCloseout, } - if params.NeedsPPMCloseout != nil && *params.NeedsPPMCloseout { ListOrderParams.Status = []string{string(models.MoveStatusAPPROVED), string(models.MoveStatusServiceCounselingCompleted)} } else { diff --git a/pkg/handlers/ghcapi/queues_test.go b/pkg/handlers/ghcapi/queues_test.go index e2c0fcb00df..2ccc4787ccf 100644 --- a/pkg/handlers/ghcapi/queues_test.go +++ b/pkg/handlers/ghcapi/queues_test.go @@ -17,6 +17,7 @@ import ( "github.com/transcom/mymove/pkg/models/roles" movelocker "github.com/transcom/mymove/pkg/services/lock_move" "github.com/transcom/mymove/pkg/services/mocks" + movefetcher "github.com/transcom/mymove/pkg/services/move" movetaskorder "github.com/transcom/mymove/pkg/services/move_task_order" officeusercreator "github.com/transcom/mymove/pkg/services/office_user" order "github.com/transcom/mymove/pkg/services/order" @@ -1640,3 +1641,170 @@ func (suite *HandlerSuite) TestGetServicesCounselingQueueHandler() { suite.Nil(payload) }) } + +func (suite *HandlerSuite) TestGetBulkAssignmentDataHandler() { + suite.Run("returns an unauthorized error when an attempt is made by a non supervisor", func() { + officeUser := factory.BuildOfficeUserWithPrivileges(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + Email: "officeuser1@example.com", + }, + }, + { + Model: models.User{ + Roles: []roles.Role{ + { + RoleType: roles.RoleTypeServicesCounselor, + }, + }, + }, + }, + }, nil) + + request := httptest.NewRequest("GET", "/queues/bulk-assignment", nil) + request = suite.AuthenticateOfficeRequest(request, officeUser) + params := queues.GetBulkAssignmentDataParams{ + HTTPRequest: request, + QueueType: models.StringPointer("COUNSELING"), + } + handlerConfig := suite.HandlerConfig() + handler := GetBulkAssignmentDataHandler{ + handlerConfig, + officeusercreator.NewOfficeUserFetcherPop(), + movefetcher.NewMoveFetcherBulkAssignment(), + } + response := handler.Handle(params) + suite.IsNotErrResponse(response) + suite.IsType(&queues.GetBulkAssignmentDataUnauthorized{}, response) + }) + suite.Run("returns properly formatted bulk assignment data", func() { + transportationOffice := factory.BuildTransportationOffice(suite.DB(), nil, nil) + + officeUser := factory.BuildOfficeUserWithPrivileges(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + Email: "officeuser1@example.com", + Active: true, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + { + Model: models.User{ + Privileges: []models.Privilege{ + { + PrivilegeType: models.PrivilegeTypeSupervisor, + }, + }, + Roles: []roles.Role{ + { + RoleType: roles.RoleTypeServicesCounselor, + }, + }, + }, + }, + }, nil) + + // move to appear in the return + factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusNeedsServiceCounseling, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + }, nil) + + request := httptest.NewRequest("GET", "/queues/bulk-assignment", nil) + request = suite.AuthenticateOfficeRequest(request, officeUser) + params := queues.GetBulkAssignmentDataParams{ + HTTPRequest: request, + QueueType: models.StringPointer("COUNSELING"), + } + handlerConfig := suite.HandlerConfig() + handler := GetBulkAssignmentDataHandler{ + handlerConfig, + officeusercreator.NewOfficeUserFetcherPop(), + movefetcher.NewMoveFetcherBulkAssignment(), + } + response := handler.Handle(params) + suite.IsNotErrResponse(response) + suite.IsType(&queues.GetBulkAssignmentDataOK{}, response) + payload := response.(*queues.GetBulkAssignmentDataOK).Payload + suite.NoError(payload.Validate(strfmt.Default)) + suite.Len(payload.AvailableOfficeUsers, 1) + suite.Len(payload.BulkAssignmentMoveIDs, 1) + }) + suite.Run("TOO: returns properly formatted bulk assignment data", func() { + transportationOffice := factory.BuildTransportationOffice(suite.DB(), nil, nil) + + officeUser := factory.BuildOfficeUserWithPrivileges(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + Email: "officeuser1@example.com", + Active: true, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + { + Model: models.User{ + Privileges: []models.Privilege{ + { + PrivilegeType: models.PrivilegeTypeSupervisor, + }, + }, + Roles: []roles.Role{ + { + RoleType: roles.RoleTypeTOO, + }, + }, + }, + }, + }, nil) + + // move to appear in the return + factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusAPPROVALSREQUESTED, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + }, nil) + + request := httptest.NewRequest("GET", "/queues/bulk-assignment", nil) + request = suite.AuthenticateOfficeRequest(request, officeUser) + params := queues.GetBulkAssignmentDataParams{ + HTTPRequest: request, + QueueType: models.StringPointer("TASK_ORDER"), + } + handlerConfig := suite.HandlerConfig() + handler := GetBulkAssignmentDataHandler{ + handlerConfig, + officeusercreator.NewOfficeUserFetcherPop(), + movefetcher.NewMoveFetcherBulkAssignment(), + } + response := handler.Handle(params) + suite.IsNotErrResponse(response) + suite.IsType(&queues.GetBulkAssignmentDataOK{}, response) + payload := response.(*queues.GetBulkAssignmentDataOK).Payload + suite.NoError(payload.Validate(strfmt.Default)) + suite.Len(payload.AvailableOfficeUsers, 1) + suite.Len(payload.BulkAssignmentMoveIDs, 1) + }) +} diff --git a/pkg/models/move.go b/pkg/models/move.go index 23c1dd7c479..a9b169e36e5 100644 --- a/pkg/models/move.go +++ b/pkg/models/move.go @@ -103,6 +103,11 @@ type Move struct { CounselingOffice *TransportationOffice `belongs_to:"transportation_offices" fk_id:"counseling_transportation_office_id"` } +type MoveWithEarliestDate struct { + ID uuid.UUID `json:"id" db:"id"` + EarliestDate time.Time `db:"earliest_date"` +} + // TableName overrides the table name used by Pop. func (m Move) TableName() string { return "moves" diff --git a/pkg/models/office_user.go b/pkg/models/office_user.go index bfbe6529123..5386b80e90b 100644 --- a/pkg/models/office_user.go +++ b/pkg/models/office_user.go @@ -44,6 +44,13 @@ type OfficeUser struct { RejectionReason *string `json:"rejection_reason" db:"rejection_reason"` } +type OfficeUserWithWorkload struct { + ID uuid.UUID `json:"id" db:"id"` + FirstName string `json:"first_name" db:"first_name"` + LastName string `json:"last_name" db:"last_name"` + Workload int `json:"workload" db:"workload"` +} + // TableName overrides the table name used by Pop. func (o OfficeUser) TableName() string { return "office_users" diff --git a/pkg/models/queue.go b/pkg/models/queue.go index d616eb8d94a..908294206b6 100644 --- a/pkg/models/queue.go +++ b/pkg/models/queue.go @@ -9,6 +9,15 @@ import ( "github.com/transcom/mymove/pkg/gen/internalmessages" ) +type QueueType string + +const ( + QueueTypeCounseling QueueType = "COUNSELING" + QueueTypeCloseout QueueType = "CLOSEOUT" + QueueTypeTaskOrder QueueType = "TASK_ORDER" + QueueTypePaymentRequest QueueType = "PAYMENT_REQUEST" +) + // MoveQueueItem represents a single move queue item within a queue. type MoveQueueItem struct { ID uuid.UUID `json:"id" db:"id"` diff --git a/pkg/services/mocks/OfficeUserFetcherPop.go b/pkg/services/mocks/OfficeUserFetcherPop.go index 24c35ca7142..2a53082a817 100644 --- a/pkg/services/mocks/OfficeUserFetcherPop.go +++ b/pkg/services/mocks/OfficeUserFetcherPop.go @@ -104,6 +104,36 @@ func (_m *OfficeUserFetcherPop) FetchOfficeUsersByRoleAndOffice(appCtx appcontex return r0, r1 } +// FetchOfficeUsersWithWorkloadByRoleAndOffice provides a mock function with given fields: appCtx, role, officeID +func (_m *OfficeUserFetcherPop) FetchOfficeUsersWithWorkloadByRoleAndOffice(appCtx appcontext.AppContext, role roles.RoleType, officeID uuid.UUID) ([]models.OfficeUserWithWorkload, error) { + ret := _m.Called(appCtx, role, officeID) + + if len(ret) == 0 { + panic("no return value specified for FetchOfficeUsersWithWorkloadByRoleAndOffice") + } + + var r0 []models.OfficeUserWithWorkload + var r1 error + if rf, ok := ret.Get(0).(func(appcontext.AppContext, roles.RoleType, uuid.UUID) ([]models.OfficeUserWithWorkload, error)); ok { + return rf(appCtx, role, officeID) + } + if rf, ok := ret.Get(0).(func(appcontext.AppContext, roles.RoleType, uuid.UUID) []models.OfficeUserWithWorkload); ok { + r0 = rf(appCtx, role, officeID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]models.OfficeUserWithWorkload) + } + } + + if rf, ok := ret.Get(1).(func(appcontext.AppContext, roles.RoleType, uuid.UUID) error); ok { + r1 = rf(appCtx, role, officeID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // FetchSafetyMoveOfficeUsersByRoleAndOffice provides a mock function with given fields: appCtx, role, officeID func (_m *OfficeUserFetcherPop) FetchSafetyMoveOfficeUsersByRoleAndOffice(appCtx appcontext.AppContext, role roles.RoleType, officeID uuid.UUID) ([]models.OfficeUser, error) { ret := _m.Called(appCtx, role, officeID) diff --git a/pkg/services/move.go b/pkg/services/move.go index 8a524accbe2..8ddda83a8ec 100644 --- a/pkg/services/move.go +++ b/pkg/services/move.go @@ -29,6 +29,11 @@ type MoveFetcher interface { FetchMovesForPPTASReports(appCtx appcontext.AppContext, params *MoveTaskOrderFetcherParams) (models.Moves, error) } +type MoveFetcherBulkAssignment interface { + FetchMovesForBulkAssignmentCounseling(appCtx appcontext.AppContext, gbloc string, officeId uuid.UUID) ([]models.MoveWithEarliestDate, error) + FetchMovesForBulkAssignmentTaskOrder(appCtx appcontext.AppContext, gbloc string, officeId uuid.UUID) ([]models.MoveWithEarliestDate, error) +} + //go:generate mockery --name MoveSearcher type MoveSearcher interface { SearchMoves(appCtx appcontext.AppContext, params *SearchMovesParams) (models.Moves, int, error) diff --git a/pkg/services/move/move_fetcher.go b/pkg/services/move/move_fetcher.go index 470f4ca78a4..a1ec1d8bbed 100644 --- a/pkg/services/move/move_fetcher.go +++ b/pkg/services/move/move_fetcher.go @@ -2,12 +2,14 @@ package move import ( "database/sql" + "fmt" "github.com/gofrs/uuid" "github.com/transcom/mymove/pkg/appcontext" "github.com/transcom/mymove/pkg/apperror" "github.com/transcom/mymove/pkg/db/utilities" + "github.com/transcom/mymove/pkg/gen/internalmessages" "github.com/transcom/mymove/pkg/models" "github.com/transcom/mymove/pkg/services" ) @@ -108,3 +110,105 @@ func (f moveFetcher) FetchMovesForPPTASReports(appCtx appcontext.AppContext, par return moves, nil } + +type moveFetcherBulkAssignment struct { +} + +// NewMoveFetcherBulkAssignment creates a new moveFetcherBulkAssignment service +func NewMoveFetcherBulkAssignment() services.MoveFetcherBulkAssignment { + return &moveFetcherBulkAssignment{} +} + +func (f moveFetcherBulkAssignment) FetchMovesForBulkAssignmentCounseling(appCtx appcontext.AppContext, gbloc string, officeId uuid.UUID) ([]models.MoveWithEarliestDate, error) { + var moves []models.MoveWithEarliestDate + + err := appCtx.DB(). + RawQuery(`SELECT + moves.id, + MIN(LEAST( + COALESCE(mto_shipments.requested_pickup_date, '9999-12-31'), + COALESCE(mto_shipments.requested_delivery_date, '9999-12-31'), + COALESCE(ppm_shipments.expected_departure_date, '9999-12-31') + )) AS earliest_date + FROM moves + INNER JOIN orders ON orders.id = moves.orders_id + INNER JOIN mto_shipments ON mto_shipments.move_id = moves.id + LEFT JOIN ppm_shipments ON ppm_shipments.shipment_id = mto_shipments.id + WHERE + moves.status = 'NEEDS SERVICE COUNSELING' + AND orders.gbloc = $1 + AND moves.show = $2 + AND moves.sc_assigned_id IS NULL + AND moves.counseling_transportation_office_id = $3 + AND (ppm_shipments.status IS NULL OR ppm_shipments.status NOT IN ($4, $5, $6)) + AND (orders.orders_type NOT IN ($7, $8, $9)) + GROUP BY moves.id + ORDER BY earliest_date ASC`, + gbloc, + models.BoolPointer(true), + officeId, + models.PPMShipmentStatusWaitingOnCustomer, + models.PPMShipmentStatusNeedsCloseout, + models.PPMShipmentStatusCloseoutComplete, + internalmessages.OrdersTypeBLUEBARK, + internalmessages.OrdersTypeWOUNDEDWARRIOR, + internalmessages.OrdersTypeSAFETY). + All(&moves) + + if err != nil { + return nil, fmt.Errorf("error fetching moves for office: %s with error %w", officeId, err) + } + + if len(moves) < 1 { + return nil, nil + } + + return moves, nil +} + +func (f moveFetcherBulkAssignment) FetchMovesForBulkAssignmentTaskOrder(appCtx appcontext.AppContext, gbloc string, officeId uuid.UUID) ([]models.MoveWithEarliestDate, error) { + var moves []models.MoveWithEarliestDate + + err := appCtx.DB(). + RawQuery(`SELECT + moves.id, + MIN(LEAST( + COALESCE(mto_shipments.requested_pickup_date, '9999-12-31'), + COALESCE(mto_shipments.requested_delivery_date, '9999-12-31'), + COALESCE(ppm_shipments.expected_departure_date, '9999-12-31') + )) AS earliest_date + FROM moves + INNER JOIN orders ON orders.id = moves.orders_id + INNER JOIN service_members ON orders.service_member_id = service_members.id + INNER JOIN mto_shipments ON mto_shipments.move_id = moves.id + LEFT JOIN ppm_shipments ON ppm_shipments.shipment_id = mto_shipments.id + LEFT JOIN move_to_gbloc ON move_to_gbloc.move_id = moves.id + WHERE + (moves.status IN ('APPROVALS REQUESTED', 'SUBMITTED', 'SERVICE COUNSELING COMPLETED')) + AND moves.show = $1 + AND moves.too_assigned_id IS NULL + AND (orders.orders_type NOT IN ($2, $3, $4)) + AND service_members.affiliation != 'MARINES' + AND ((mto_shipments.shipment_type != $5 AND move_to_gbloc.gbloc = $6) OR (mto_shipments.shipment_type = $7 AND orders.gbloc = $8)) + GROUP BY moves.id + ORDER BY earliest_date ASC`, + models.BoolPointer(true), + internalmessages.OrdersTypeBLUEBARK, + internalmessages.OrdersTypeWOUNDEDWARRIOR, + internalmessages.OrdersTypeSAFETY, + models.MTOShipmentTypeHHGOutOfNTSDom, + gbloc, + models.MTOShipmentTypeHHGOutOfNTSDom, + gbloc). + All(&moves) + + if err != nil { + return nil, fmt.Errorf("error fetching moves for office: %s with error %w", officeId, err) + } + + if len(moves) < 1 { + return nil, nil + } + + return moves, nil +} diff --git a/pkg/services/move/move_fetcher_test.go b/pkg/services/move/move_fetcher_test.go index 9901f8a260e..92fb23751bb 100644 --- a/pkg/services/move/move_fetcher_test.go +++ b/pkg/services/move/move_fetcher_test.go @@ -5,7 +5,9 @@ import ( "github.com/transcom/mymove/pkg/apperror" "github.com/transcom/mymove/pkg/factory" + "github.com/transcom/mymove/pkg/gen/internalmessages" "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/models/roles" "github.com/transcom/mymove/pkg/services" ) @@ -102,3 +104,394 @@ func (suite *MoveServiceSuite) TestMoveFetcher() { suite.Equal(expectedMove.Locator, actualMove.Locator) }) } + +func (suite *MoveServiceSuite) TestMoveFetcherBulkAssignment() { + setupTestData := func() (services.MoveFetcherBulkAssignment, models.Move, models.TransportationOffice, models.OfficeUser) { + moveFetcher := NewMoveFetcherBulkAssignment() + transportationOffice := factory.BuildTransportationOffice(suite.DB(), nil, nil) + + // this move has a transportation office associated with it that matches + // the SC's transportation office and should be found + move := factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusNeedsServiceCounseling, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + }, nil) + + factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusNeedsServiceCounseling, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + }, nil) + + officeUser := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + }, []roles.RoleType{roles.RoleTypeServicesCounselor}) + + return moveFetcher, move, transportationOffice, officeUser + } + + suite.Run("Returns moves that fulfill the query's criteria", func() { + moveFetcher, _, _, officeUser := setupTestData() + moves, err := moveFetcher.FetchMovesForBulkAssignmentCounseling(suite.AppContextForTest(), "KKFA", officeUser.TransportationOffice.ID) + suite.FatalNoError(err) + suite.Equal(2, len(moves)) + }) + + suite.Run("Does not return moves that are counseled by a different counseling office", func() { + moveFetcher, _, _, officeUser := setupTestData() + transportationOffice := factory.BuildTransportationOffice(suite.DB(), nil, nil) + + factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusNeedsServiceCounseling, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + }, nil) + + moves, err := moveFetcher.FetchMovesForBulkAssignmentCounseling(suite.AppContextForTest(), "KKFA", officeUser.TransportationOffice.ID) + suite.FatalNoError(err) + suite.Equal(2, len(moves)) + }) + + suite.Run("Does not return moves with safety, bluebark, or wounded warrior order types", func() { + moveFetcher, _, transportationOffice, officeUser := setupTestData() + factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ + { + Model: models.Order{ + OrdersType: internalmessages.OrdersTypeSAFETY, + }, + }, + { + Model: models.Move{ + Status: models.MoveStatusNeedsServiceCounseling, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + }, nil) + factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ + { + Model: models.Order{ + OrdersType: internalmessages.OrdersTypeBLUEBARK, + }, + }, + { + Model: models.Move{ + Status: models.MoveStatusNeedsServiceCounseling, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + }, nil) + factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ + { + Model: models.Order{ + OrdersType: internalmessages.OrdersTypeWOUNDEDWARRIOR, + }, + }, + { + Model: models.Move{ + Status: models.MoveStatusNeedsServiceCounseling, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + }, nil) + + moves, err := moveFetcher.FetchMovesForBulkAssignmentCounseling(suite.AppContextForTest(), "KKFA", officeUser.TransportationOffice.ID) + suite.FatalNoError(err) + suite.Equal(2, len(moves)) + }) + + // BuildMoveWithPPMShipment apparently builds 3 moves each time its run, so the best way + // to test is to make sure that the moveWithPPM move is not returned in these 3 separate tests + suite.Run("Does not return moves with PPMs in waiting on customer status", func() { + moveFetcher := NewMoveFetcherBulkAssignment() + transportationOffice := factory.BuildTransportationOffice(suite.DB(), nil, nil) + moveWithWaitingOnCustomerPPM := factory.BuildMoveWithPPMShipment(suite.DB(), []factory.Customization{ + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + { + Model: models.PPMShipment{ + Status: models.PPMShipmentStatusWaitingOnCustomer, + }, + }, + }, []factory.Trait{factory.GetTraitNeedsServiceCounselingMove}) + + officeUser := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + }, []roles.RoleType{roles.RoleTypeServicesCounselor}) + + moves, err := moveFetcher.FetchMovesForBulkAssignmentCounseling(suite.AppContextForTest(), "KKFA", officeUser.TransportationOffice.ID) + suite.FatalNoError(err) + // confirm that the there is only one move appearing + suite.Equal(1, len(moves)) + // confirm that the move appearing iS NOT the moveWithPPM + suite.NotEqual(moves[0].ID, moveWithWaitingOnCustomerPPM.ID) + // confirm that the rest of the details are correct + // and that it SHOULD show up in the queue if it wasn't for PPM status + // move is NEEDS SERVICE COUNSELING STATUS + suite.Equal(moveWithWaitingOnCustomerPPM.Status, models.MoveStatusNeedsServiceCounseling) + // move is not assigned to anyone + suite.Nil(moveWithWaitingOnCustomerPPM.SCAssignedID) + // GBLOC is the same + suite.Equal(*moveWithWaitingOnCustomerPPM.Orders.OriginDutyLocationGBLOC, officeUser.TransportationOffice.Gbloc) + // Show is true + suite.Equal(moveWithWaitingOnCustomerPPM.Show, models.BoolPointer(true)) + // Move is counseled by the office user's office + suite.Equal(*moveWithWaitingOnCustomerPPM.CounselingOfficeID, officeUser.TransportationOfficeID) + // Orders type isn't WW, BB, or Safety + suite.Equal(moveWithWaitingOnCustomerPPM.Orders.OrdersType, internalmessages.OrdersTypePERMANENTCHANGEOFSTATION) + }) + + suite.Run("Does not return moves with PPMs in needs closeout status", func() { + moveFetcher := NewMoveFetcherBulkAssignment() + transportationOffice := factory.BuildTransportationOffice(suite.DB(), nil, nil) + moveWithNeedsCloseoutPPM := factory.BuildMoveWithPPMShipment(suite.DB(), []factory.Customization{ + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + { + Model: models.PPMShipment{ + Status: models.PPMShipmentStatusNeedsCloseout, + }, + }, + }, []factory.Trait{factory.GetTraitNeedsServiceCounselingMove}) + + officeUser := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + }, []roles.RoleType{roles.RoleTypeServicesCounselor}) + + moves, err := moveFetcher.FetchMovesForBulkAssignmentCounseling(suite.AppContextForTest(), "KKFA", officeUser.TransportationOffice.ID) + suite.FatalNoError(err) + // confirm that the there is only one move appearing + suite.Equal(1, len(moves)) + // confirm that the move appearing iS NOT the moveWithPPM + suite.NotEqual(moves[0].ID, moveWithNeedsCloseoutPPM.ID) + // confirm that the rest of the details are correct + // and that it SHOULD show up in the queue if it wasn't for PPM status + // move is NEEDS SERVICE COUNSELING STATUS + suite.Equal(moveWithNeedsCloseoutPPM.Status, models.MoveStatusNeedsServiceCounseling) + // move is not assigned to anyone + suite.Nil(moveWithNeedsCloseoutPPM.SCAssignedID) + // GBLOC is the same + suite.Equal(*moveWithNeedsCloseoutPPM.Orders.OriginDutyLocationGBLOC, officeUser.TransportationOffice.Gbloc) + // Show is true + suite.Equal(moveWithNeedsCloseoutPPM.Show, models.BoolPointer(true)) + // Move is counseled by the office user's office + suite.Equal(*moveWithNeedsCloseoutPPM.CounselingOfficeID, officeUser.TransportationOfficeID) + // Orders type isn't WW, BB, or Safety + suite.Equal(moveWithNeedsCloseoutPPM.Orders.OrdersType, internalmessages.OrdersTypePERMANENTCHANGEOFSTATION) + }) + suite.Run("Does not return moves with PPMs in closeout complete status", func() { + moveFetcher := NewMoveFetcherBulkAssignment() + transportationOffice := factory.BuildTransportationOffice(suite.DB(), []factory.Customization{ + { + Model: models.TransportationOffice{ + ProvidesCloseout: true, + }, + }, + }, nil) + moveWithCloseoutCompletePPM := factory.BuildMoveWithPPMShipment(suite.DB(), []factory.Customization{ + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + { + Model: models.PPMShipment{ + Status: models.PPMShipmentStatusCloseoutComplete, + }, + }, + }, []factory.Trait{factory.GetTraitNeedsServiceCounselingMove}) + + officeUser := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + }, []roles.RoleType{roles.RoleTypeServicesCounselor}) + + moves, err := moveFetcher.FetchMovesForBulkAssignmentCounseling(suite.AppContextForTest(), "KKFA", officeUser.TransportationOffice.ID) + suite.FatalNoError(err) + // confirm that the there is only one move appearing + suite.Equal(1, len(moves)) + // confirm that the move appearing iS NOT the moveWithPPM + suite.NotEqual(moves[0].ID, moveWithCloseoutCompletePPM.ID) + // confirm that the rest of the details are correct + // and that it SHOULD show up in the queue if it wasn't for PPM status + // move is NEEDS SERVICE COUNSELING STATUS + suite.Equal(moveWithCloseoutCompletePPM.Status, models.MoveStatusNeedsServiceCounseling) + // move is not assigned to anyone + suite.Nil(moveWithCloseoutCompletePPM.SCAssignedID) + // GBLOC is the same + suite.Equal(*moveWithCloseoutCompletePPM.Orders.OriginDutyLocationGBLOC, officeUser.TransportationOffice.Gbloc) + // Show is true + suite.Equal(moveWithCloseoutCompletePPM.Show, models.BoolPointer(true)) + // Move is counseled by the office user's office + suite.Equal(*moveWithCloseoutCompletePPM.CounselingOfficeID, officeUser.TransportationOfficeID) + // Orders type isn't WW, BB, or Safety + suite.Equal(moveWithCloseoutCompletePPM.Orders.OrdersType, internalmessages.OrdersTypePERMANENTCHANGEOFSTATION) + }) + + suite.Run("Does not return moves that are already assigned", func() { + // moveFetcher, _, transOffice, officeUser := setupTestData() + moveFetcher := NewMoveFetcherBulkAssignment() + transportationOffice := factory.BuildTransportationOffice(suite.DB(), nil, nil) + + officeUser := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + }, []roles.RoleType{roles.RoleTypeServicesCounselor}) + + assignedMove := factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusNeedsServiceCounseling, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + { + Model: officeUser, + LinkOnly: true, + Type: &factory.OfficeUsers.SCAssignedUser, + }, + }, nil) + + moves, err := moveFetcher.FetchMovesForBulkAssignmentCounseling(suite.AppContextForTest(), "KKFA", officeUser.TransportationOffice.ID) + suite.FatalNoError(err) + + // confirm that the assigned move isn't returned + for _, move := range moves { + suite.NotEqual(move.ID, assignedMove.ID) + } + + // confirm that the rest of the details are correct + // move is NEEDS SERVICE COUNSELING STATUS + suite.Equal(assignedMove.Status, models.MoveStatusNeedsServiceCounseling) + // GBLOC is the same + suite.Equal(*assignedMove.Orders.OriginDutyLocationGBLOC, officeUser.TransportationOffice.Gbloc) + // Show is true + suite.Equal(assignedMove.Show, models.BoolPointer(true)) + // Move is counseled by the office user's office + suite.Equal(*assignedMove.CounselingOfficeID, officeUser.TransportationOfficeID) + // Orders type isn't WW, BB, or Safety + suite.Equal(assignedMove.Orders.OrdersType, internalmessages.OrdersTypePERMANENTCHANGEOFSTATION) + }) + + suite.Run("TOO: Returns moves that fulfill the query criteria", func() { + moveFetcher := NewMoveFetcherBulkAssignment() + transportationOffice := factory.BuildTransportationOffice(suite.DB(), nil, nil) + officeUser := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + }, []roles.RoleType{roles.RoleTypeTOO}) + + factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusAPPROVALSREQUESTED, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + }, nil) + + factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusServiceCounselingCompleted, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + }, nil) + + marine := models.AffiliationMARINES + factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusServiceCounselingCompleted, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + { + Model: models.ServiceMember{ + Affiliation: &marine, + }, + }, + }, nil) + + moves, err := moveFetcher.FetchMovesForBulkAssignmentTaskOrder(suite.AppContextForTest(), "KKFA", officeUser.TransportationOffice.ID) + suite.FatalNoError(err) + suite.Equal(2, len(moves)) + }) +} diff --git a/pkg/services/office_user.go b/pkg/services/office_user.go index 94e5410d3ab..c0a84c99e20 100644 --- a/pkg/services/office_user.go +++ b/pkg/services/office_user.go @@ -25,6 +25,7 @@ type OfficeUserFetcherPop interface { FetchOfficeUserByIDWithTransportationOfficeAssignments(appCtx appcontext.AppContext, id uuid.UUID) (models.OfficeUser, error) FetchOfficeUsersByRoleAndOffice(appCtx appcontext.AppContext, role roles.RoleType, officeID uuid.UUID) ([]models.OfficeUser, error) FetchSafetyMoveOfficeUsersByRoleAndOffice(appCtx appcontext.AppContext, role roles.RoleType, officeID uuid.UUID) ([]models.OfficeUser, error) + FetchOfficeUsersWithWorkloadByRoleAndOffice(appCtx appcontext.AppContext, role roles.RoleType, officeID uuid.UUID) ([]models.OfficeUserWithWorkload, error) } // OfficeUserGblocFetcher is the exported interface for fetching the GBLOC of the diff --git a/pkg/services/office_user/office_user_fetcher.go b/pkg/services/office_user/office_user_fetcher.go index 78f310b8a5e..066ff342111 100644 --- a/pkg/services/office_user/office_user_fetcher.go +++ b/pkg/services/office_user/office_user_fetcher.go @@ -2,6 +2,7 @@ package officeuser import ( "database/sql" + "fmt" "github.com/gobuffalo/validate/v3" "github.com/gofrs/uuid" @@ -130,6 +131,38 @@ func (o *officeUserFetcherPop) FetchSafetyMoveOfficeUsersByRoleAndOffice(appCtx return officeUsers, nil } +// Fetch office users of the same role within a gbloc, with their workload, for assignment purposes +func (o *officeUserFetcherPop) FetchOfficeUsersWithWorkloadByRoleAndOffice(appCtx appcontext.AppContext, role roles.RoleType, officeID uuid.UUID) ([]models.OfficeUserWithWorkload, error) { + var officeUsers []models.OfficeUserWithWorkload + + query := `SELECT ou.id, + ou.first_name, + ou.last_name, + COUNT(m.id) AS workload + FROM office_users AS ou + JOIN users AS u on ou.user_id = u.id + JOIN users_roles AS ur on u.id = ur.user_id + JOIN roles as R on ur.role_id = r.id + JOIN transportation_offices on ou.transportation_office_id = transportation_offices.id + LEFT JOIN moves AS m + ON ( + (r.role_type = 'services_counselor' AND m.sc_assigned_id = ou.id) OR + (r.role_type = 'task_ordering_officer' AND m.too_assigned_id = ou.id) OR + (r.role_type = 'task_invoicing_officer' and m.tio_assigned_id = ou.id) + ) + WHERE r.role_type = $1 + AND transportation_offices.id = $2 + AND ou.active = TRUE + GROUP BY ou.id, ou.first_name, ou.last_name` + err := appCtx.DB().RawQuery(query, role, officeID).All(&officeUsers) + + if err != nil { + return nil, fmt.Errorf("error fetching moves for office: %s with error %w", officeID, err) + } + + return officeUsers, nil +} + // NewOfficeUserFetcherPop return an implementation of the OfficeUserFetcherPop interface func NewOfficeUserFetcherPop() services.OfficeUserFetcherPop { return &officeUserFetcherPop{} diff --git a/pkg/services/office_user/office_user_fetcher_test.go b/pkg/services/office_user/office_user_fetcher_test.go index 5ce6c1aeda4..f5a82564a69 100644 --- a/pkg/services/office_user/office_user_fetcher_test.go +++ b/pkg/services/office_user/office_user_fetcher_test.go @@ -83,8 +83,8 @@ func (suite *OfficeUserServiceSuite) TestFetchOfficeUser() { }) } -func (suite *OfficeUserServiceSuite) TestFetchOfficeUserPop() { - suite.Run("returns office user on success", func() { +func (suite *OfficeUserServiceSuite) TestFetchOfficeUserByID() { + suite.Run("FetchOfficeUserByID returns office user on success", func() { officeUser := factory.BuildOfficeUserWithRoles(suite.DB(), nil, []roles.RoleType{roles.RoleTypeTOO}) fetcher := NewOfficeUserFetcherPop() @@ -94,7 +94,60 @@ func (suite *OfficeUserServiceSuite) TestFetchOfficeUserPop() { suite.Equal(officeUser.ID, fetchedUser.ID) }) - suite.Run("returns a set of office users when given a and role", func() { + suite.Run("FetchOfficeUserByID returns zero value office user on error", func() { + fetcher := NewOfficeUserFetcherPop() + officeUser, err := fetcher.FetchOfficeUserByID(suite.AppContextForTest(), uuid.Nil) + + suite.Error(err) + suite.IsType(apperror.NotFoundError{}, err) + suite.Equal(uuid.Nil, officeUser.ID) + }) +} + +func (suite *OfficeUserServiceSuite) TestFetchOfficeUsersWithWorkloadByRoleAndOffice() { + fetcher := NewOfficeUserFetcherPop() + suite.Run("FetchOfficeUsersWithWorkloadByRoleAndOffice returns an active office user's name, id, and workload when given a role and office", func() { + transportationOffice := factory.BuildTransportationOffice(suite.DB(), nil, nil) + + officeUser := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + { + Model: models.OfficeUser{ + Active: true, + }, + }, + }, []roles.RoleType{roles.RoleTypeServicesCounselor}) + + factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusNeedsServiceCounseling, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + { + Model: officeUser, + LinkOnly: true, + Type: &factory.OfficeUsers.SCAssignedUser, + }, + }, nil) + + fetchedUsers, err := fetcher.FetchOfficeUsersWithWorkloadByRoleAndOffice(suite.AppContextForTest(), roles.RoleTypeServicesCounselor, officeUser.TransportationOfficeID) + suite.NoError(err) + fetchedOfficeUser := fetchedUsers[0] + suite.Equal(officeUser.ID, fetchedOfficeUser.ID) + suite.Equal(1, fetchedOfficeUser.Workload) + }) + + suite.Run("FetchOfficeUsersByRoleAndOffice returns a set of office users when given an office and role", func() { // build 1 TOO officeUser := factory.BuildOfficeUserWithRoles(suite.DB(), factory.GetTraitActiveOfficeUser(), []roles.RoleType{roles.RoleTypeTOO}) // build 2 SCs and 3 TIOs @@ -103,7 +156,6 @@ func (suite *OfficeUserServiceSuite) TestFetchOfficeUserPop() { factory.BuildOfficeUserWithRoles(suite.DB(), factory.GetTraitActiveOfficeUser(), []roles.RoleType{roles.RoleTypeTIO}) factory.BuildOfficeUserWithRoles(suite.DB(), factory.GetTraitActiveOfficeUser(), []roles.RoleType{roles.RoleTypeTIO}) factory.BuildOfficeUserWithRoles(suite.DB(), factory.GetTraitActiveOfficeUser(), []roles.RoleType{roles.RoleTypeTIO}) - fetcher := NewOfficeUserFetcherPop() fetchedUsers, err := fetcher.FetchOfficeUsersByRoleAndOffice(suite.AppContextForTest(), roles.RoleTypeTOO, officeUser.TransportationOfficeID) @@ -114,7 +166,6 @@ func (suite *OfficeUserServiceSuite) TestFetchOfficeUserPop() { }) suite.Run("returns zero value office user on error", func() { - fetcher := NewOfficeUserFetcherPop() officeUser, err := fetcher.FetchOfficeUserByID(suite.AppContextForTest(), uuid.Nil) suite.Error(err) diff --git a/src/constants/queryKeys.js b/src/constants/queryKeys.js index 40f125c5023..14f29abb837 100644 --- a/src/constants/queryKeys.js +++ b/src/constants/queryKeys.js @@ -17,6 +17,7 @@ export const CUSTOMER = 'customer'; export const PRIME_SIMULATOR_AVAILABLE_MOVES = 'listMoves'; export const PRIME_SIMULATOR_MOVE = 'primeMove'; export const CUSTOMER_SUPPORT_REMARKS = 'customerSupportRemarks'; +export const BULK_ASSIGNMENT = 'bulkAssignment'; export const QAE_MOVE_SEARCH = 'qaeMoveSearch'; export const SHIPMENT_EVALUATION_REPORTS = 'shipmentEvaluationReports'; export const COUNSELING_EVALUATION_REPORTS = 'counselingEvaluationReports'; diff --git a/src/constants/queues.js b/src/constants/queues.js index c17d53645bd..1ee59cd111d 100644 --- a/src/constants/queues.js +++ b/src/constants/queues.js @@ -88,6 +88,13 @@ export const GBLOC = { USMC: 'USMC', }; +export const QUEUE_TYPES = { + COUNSELING: 'COUNSELING', + CLOSEOUT: 'CLOSEOUT', + TASK_ORDER: 'TASK_ORDER', + PAYMENT_REQUEST: 'PAYMENT_REQUEST', +}; + export const PAGINATION_PAGE_DEFAULT = 1; export const PAGINATION_PAGE_SIZE_DEFAULT = 20; diff --git a/src/hooks/queries.js b/src/hooks/queries.js index e46b708e669..ce469702359 100644 --- a/src/hooks/queries.js +++ b/src/hooks/queries.js @@ -34,6 +34,7 @@ import { getPPMActualWeight, searchCustomers, getGBLOCs, + getBulkAssignmentData, } from 'services/ghcApi'; import { getLoggedInUserQueries } from 'services/internalApi'; import { getPrimeSimulatorMove } from 'services/primeApi'; @@ -220,6 +221,19 @@ export const useCustomerSupportRemarksQueries = (moveCode) => { }; }; +export const useBulkAssignmentQueries = (queueType) => { + const { data: bulkAssignmentData, ...bulkAssignmentDataQuery } = useQuery([queueType], ({ queryKey }) => + getBulkAssignmentData(queryKey), + ); + const { isLoading, isError, isSuccess } = getQueriesStatus([bulkAssignmentDataQuery]); + return { + bulkAssignmentData, + isLoading, + isError, + isSuccess, + }; +}; + export const useEditShipmentQueries = (moveCode) => { // Get the orders info const { data: move = {}, ...moveQuery } = useQuery([MOVES, moveCode], ({ queryKey }) => getMove(...queryKey)); diff --git a/src/services/ghcApi.js b/src/services/ghcApi.js index d4d8f3e763a..aab855315c7 100644 --- a/src/services/ghcApi.js +++ b/src/services/ghcApi.js @@ -142,6 +142,10 @@ export async function getCustomerSupportRemarksForMove(key, locator) { return makeGHCRequest('customerSupportRemarks.getCustomerSupportRemarksForMove', { locator }, { normalize: false }); } +export async function getBulkAssignmentData(queueType) { + return makeGHCRequest('queues.getBulkAssignmentData', { queueType }, { normalize: false }); +} + export async function createCustomerSupportRemarkForMove({ body, locator }) { return makeGHCRequest('customerSupportRemarks.createCustomerSupportRemarkForMove', { body, diff --git a/swagger-def/ghc.yaml b/swagger-def/ghc.yaml index a0df07f3800..3d965ec0a5c 100644 --- a/swagger-def/ghc.yaml +++ b/swagger-def/ghc.yaml @@ -3471,6 +3471,37 @@ paths: $ref: '#/responses/PermissionDenied' '500': $ref: '#/responses/ServerError' + /queues/bulk-assignment: + get: + produces: + - application/json + summary: Gets data for bulk assignment modal + description: > + Supervisor office users are able to bulk assign moves. This endpoint returns the relevant data to them; the current workload of the office users that work under them, and the moves that are available to be assigned + operationId: getBulkAssignmentData + tags: + - queues + parameters: + - in: query + name: queueType + type: string + description: A string corresponding to the queue type + enum: + - COUNSELING + - CLOSEOUT + - TASK_ORDER + - PAYMENT_REQUEST + responses: + '200': + description: Successfully returned bulk assignment data + schema: + $ref: '#/definitions/BulkAssignmentData' + '401': + $ref: '#/responses/PermissionDenied' + '404': + $ref: '#/responses/NotFound' + '500': + $ref: '#/responses/ServerError' /queues/counseling/origin-list: get: produces: @@ -6977,6 +7008,14 @@ definitions: eTag: type: string type: object + BulkAssignmentMoveIDs: + type: array + items: + $ref: '#/definitions/BulkAssignmentMoveID' + BulkAssignmentMoveID: + type: string + format: uuid + example: c56a4180-65aa-42ec-a945-5fd21dec0538 AvailableOfficeUsers: type: array items: @@ -6992,6 +7031,17 @@ definitions: type: string firstName: type: string + hasSafetyPrivilege: + type: boolean + workload: + type: integer + BulkAssignmentData: + type: object + properties: + availableOfficeUsers: + $ref: '#/definitions/AvailableOfficeUsers' + bulkAssignmentMoveIDs: + $ref: '#/definitions/BulkAssignmentMoveIDs' QueueMoves: type: array items: diff --git a/swagger/ghc.yaml b/swagger/ghc.yaml index ab568b434c8..9eccd1ab98b 100644 --- a/swagger/ghc.yaml +++ b/swagger/ghc.yaml @@ -3600,6 +3600,40 @@ paths: $ref: '#/responses/PermissionDenied' '500': $ref: '#/responses/ServerError' + /queues/bulk-assignment: + get: + produces: + - application/json + summary: Gets data for bulk assignment modal + description: > + Supervisor office users are able to bulk assign moves. This endpoint + returns the relevant data to them; the current workload of the office + users that work under them, and the moves that are available to be + assigned + operationId: getBulkAssignmentData + tags: + - queues + parameters: + - in: query + name: queueType + type: string + description: A string corresponding to the queue type + enum: + - COUNSELING + - CLOSEOUT + - TASK_ORDER + - PAYMENT_REQUEST + responses: + '200': + description: Successfully returned bulk assignment data + schema: + $ref: '#/definitions/BulkAssignmentData' + '401': + $ref: '#/responses/PermissionDenied' + '404': + $ref: '#/responses/NotFound' + '500': + $ref: '#/responses/ServerError' /queues/counseling/origin-list: get: produces: @@ -7314,6 +7348,14 @@ definitions: eTag: type: string type: object + BulkAssignmentMoveIDs: + type: array + items: + $ref: '#/definitions/BulkAssignmentMoveID' + BulkAssignmentMoveID: + type: string + format: uuid + example: c56a4180-65aa-42ec-a945-5fd21dec0538 AvailableOfficeUsers: type: array items: @@ -7329,6 +7371,17 @@ definitions: type: string firstName: type: string + hasSafetyPrivilege: + type: boolean + workload: + type: integer + BulkAssignmentData: + type: object + properties: + availableOfficeUsers: + $ref: '#/definitions/AvailableOfficeUsers' + bulkAssignmentMoveIDs: + $ref: '#/definitions/BulkAssignmentMoveIDs' QueueMoves: type: array items: