From 20f707652e5a99bc30052c66445527f288b5455a Mon Sep 17 00:00:00 2001 From: Paul Stonebraker Date: Wed, 11 Dec 2024 17:22:37 +0000 Subject: [PATCH 01/18] implement fetching bulk assignment data --- pkg/factory/move_factory.go | 30 ++ pkg/factory/office_user_factory.go | 33 +- pkg/factory/shared.go | 2 + pkg/gen/ghcapi/configure_mymove.go | 5 + pkg/gen/ghcapi/embedded_spec.go | 153 +++++++++ pkg/gen/ghcapi/ghcoperations/mymove_api.go | 12 + .../queues/get_bulk_assignment_data.go | 58 ++++ .../get_bulk_assignment_data_parameters.go | 92 +++++ .../get_bulk_assignment_data_responses.go | 194 +++++++++++ .../get_bulk_assignment_data_urlbuilder.go | 103 ++++++ pkg/gen/ghcmessages/available_office_user.go | 6 + pkg/gen/ghcmessages/bulk_assignment_data.go | 142 ++++++++ .../ghcmessages/bulk_assignment_move_i_ds.go | 70 ++++ .../ghcmessages/bulk_assignment_move_id.go | 39 +++ pkg/handlers/ghcapi/api.go | 6 + .../internal/payloads/model_to_payload.go | 24 ++ pkg/handlers/ghcapi/queues.go | 60 ++++ pkg/models/move.go | 5 + pkg/models/office_user.go | 7 + pkg/models/queue.go | 9 + pkg/services/move.go | 5 + pkg/services/move/move_fetcher.go | 89 +++++ pkg/services/move/move_fetcher_test.go | 320 ++++++++++++++++++ pkg/services/office_user.go | 1 + .../office_user/office_user_fetcher.go | 32 ++ .../BulkAssignment/BulkAssignmentModal.jsx | 45 +++ src/components/Table/TableQueue.jsx | 24 +- src/constants/queryKeys.js | 1 + src/constants/queues.js | 7 + src/hooks/queries.js | 14 + src/services/ghcApi.js | 4 + swagger-def/ghc.yaml | 50 +++ swagger/ghc.yaml | 53 +++ 33 files changed, 1684 insertions(+), 11 deletions(-) create mode 100644 pkg/gen/ghcapi/ghcoperations/queues/get_bulk_assignment_data.go create mode 100644 pkg/gen/ghcapi/ghcoperations/queues/get_bulk_assignment_data_parameters.go create mode 100644 pkg/gen/ghcapi/ghcoperations/queues/get_bulk_assignment_data_responses.go create mode 100644 pkg/gen/ghcapi/ghcoperations/queues/get_bulk_assignment_data_urlbuilder.go create mode 100644 pkg/gen/ghcmessages/bulk_assignment_data.go create mode 100644 pkg/gen/ghcmessages/bulk_assignment_move_i_ds.go create mode 100644 pkg/gen/ghcmessages/bulk_assignment_move_id.go create mode 100644 src/components/BulkAssignment/BulkAssignmentModal.jsx diff --git a/pkg/factory/move_factory.go b/pkg/factory/move_factory.go index e2192ae93f0..16233c288f7 100644 --- a/pkg/factory/move_factory.go +++ b/pkg/factory/move_factory.go @@ -1,6 +1,7 @@ package factory import ( + "fmt" "log" "time" @@ -71,6 +72,8 @@ func BuildMove(db *pop.Connection, customs []Customization, traits []Trait) mode if closeoutOfficeResult != nil { move.CloseoutOffice = &closeoutOffice move.CloseoutOfficeID = &closeoutOffice.ID + move.CounselingOffice = &closeoutOffice + move.CounselingOfficeID = &closeoutOffice.ID } // Overwrite values with those from assertions @@ -181,9 +184,36 @@ func BuildMoveWithPPMShipment(db *pop.Connection, customs []Customization, trait if db != nil { mustSave(db, &move) } + fmt.Println("move in factory") + fmt.Println(move.ID) return move } +func BuildAssignedMoveWithPPMShipment(db *pop.Connection, customs []Customization, traits []Trait) models.Move { + move := BuildMove(db, customs, traits) + + mtoShipment := buildMTOShipmentWithBuildType(db, customs, traits, mtoShipmentPPM) + mtoShipment.MoveTaskOrder = move + mtoShipment.MoveTaskOrderID = move.ID + + ppmShipment := BuildPPMShipment(db, customs, traits) + ppmShipment.ShipmentID = mtoShipment.ID + + mtoShipment.PPMShipment = &ppmShipment + mtoShipment.ShipmentType = models.MTOShipmentTypePPM + move.MTOShipments = append(move.MTOShipments, mtoShipment) + + officeUser := BuildOfficeUser(db, customs, traits) + move.SCAssignedUser = &officeUser + move.SCAssignedID = &officeUser.ID + + if db != nil { + mustSave(db, &move) + } + fmt.Println("move") + fmt.Println(move.SCAssignedUser) + return move +} // ------------------------ // TRAITS diff --git a/pkg/factory/office_user_factory.go b/pkg/factory/office_user_factory.go index b995c81b408..fbd9b2cc784 100644 --- a/pkg/factory/office_user_factory.go +++ b/pkg/factory/office_user_factory.go @@ -37,19 +37,34 @@ func BuildOfficeUser(db *pop.Connection, customs []Customization, traits []Trait // Find/create the user model user := BuildUserAndUsersRoles(db, customs, nil) + var closeoutOffice models.TransportationOffice + tempCloseoutOfficeCustoms := customs + closeoutOfficeResult := findValidCustomization(customs, TransportationOffices.CloseoutOffice) + if closeoutOfficeResult != nil { + tempCloseoutOfficeCustoms = convertCustomizationInList(tempCloseoutOfficeCustoms, TransportationOffices.CloseoutOffice, TransportationOffice) + closeoutOffice = BuildTransportationOffice(db, tempCloseoutOfficeCustoms, nil) + } // Find/create the TransportationOffice model - transportationOffice := BuildTransportationOffice(db, customs, nil) + basicTransportationOffice := BuildTransportationOffice(db, customs, nil) // create officeuser officeUser := models.OfficeUser{ - UserID: &user.ID, - User: user, - FirstName: "Leo", - LastName: "Spaceman", - Email: "leo_spaceman_office@example.com", - Telephone: "415-555-1212", - TransportationOffice: transportationOffice, - TransportationOfficeID: transportationOffice.ID, + UserID: &user.ID, + User: user, + FirstName: "Leo", + LastName: "Spaceman", + Email: "leo_spaceman_office@example.com", + Telephone: "415-555-1212", + // TransportationOffice: transportationOffice, + // TransportationOfficeID: transportationOffice.ID, + } + + if closeoutOfficeResult != nil { + officeUser.TransportationOffice = closeoutOffice + officeUser.TransportationOfficeID = closeoutOffice.ID + } else { + officeUser.TransportationOffice = basicTransportationOffice + officeUser.TransportationOfficeID = basicTransportationOffice.ID } // Overwrite values with those from assertions testdatagen.MergeModels(&officeUser, cOfficeUser) diff --git a/pkg/factory/shared.go b/pkg/factory/shared.go index a4888a31588..a6eca09823a 100644 --- a/pkg/factory/shared.go +++ b/pkg/factory/shared.go @@ -252,6 +252,7 @@ type transportationOfficeGroup struct { OriginDutyLocation CustomType NewDutyLocation CustomType CloseoutOffice CustomType + CounselingOffice CustomType } // TransportationOffices is the struct to access the fields externally @@ -259,6 +260,7 @@ var TransportationOffices = transportationOfficeGroup{ OriginDutyLocation: "OriginDutyLocationTransportationOffice", NewDutyLocation: "NewDutyLocationTransportationOffice", CloseoutOffice: "CloseoutOffice", + CounselingOffice: "CounselingOffice", } // 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 4ca4c62707f..bcccaef48d6 100644 --- a/pkg/gen/ghcapi/configure_mymove.go +++ b/pkg/gen/ghcapi/configure_mymove.go @@ -221,6 +221,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 111597a6ea9..f4d48668f5f 100644 --- a/pkg/gen/ghcapi/embedded_spec.go +++ b/pkg/gen/ghcapi/embedded_spec.go @@ -4274,6 +4274,50 @@ func init() { } } }, + "/queues/bulk-assignment": { + "get": { + "description": "Supervisor office users are able to bulk assign moves. This endpoint returns the relvant 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", @@ -6638,6 +6682,9 @@ func init() { "firstName": { "type": "string" }, + "hasSafetyPrivilege": { + "type": "boolean" + }, "lastName": { "type": "string" }, @@ -6645,6 +6692,9 @@ func init() { "type": "string", "format": "uuid", "example": "c56a4180-65aa-42ec-a945-5fd21dec0538" + }, + "workload": { + "type": "integer" } } }, @@ -6767,6 +6817,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": [ @@ -20359,6 +20431,59 @@ func init() { } } }, + "/queues/bulk-assignment": { + "get": { + "description": "Supervisor office users are able to bulk assign moves. This endpoint returns the relvant 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", @@ -23117,6 +23242,9 @@ func init() { "firstName": { "type": "string" }, + "hasSafetyPrivilege": { + "type": "boolean" + }, "lastName": { "type": "string" }, @@ -23124,6 +23252,9 @@ func init() { "type": "string", "format": "uuid", "example": "c56a4180-65aa-42ec-a945-5fd21dec0538" + }, + "workload": { + "type": "integer" } } }, @@ -23246,6 +23377,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 0809cb3b701..318d7833018 100644 --- a/pkg/gen/ghcapi/ghcoperations/mymove_api.go +++ b/pkg/gen/ghcapi/ghcoperations/mymove_api.go @@ -160,6 +160,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") }), @@ -491,6 +494,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 @@ -814,6 +819,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") } @@ -1254,6 +1262,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..e9ffdc1dc42 --- /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 relvant 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 779c20950ef..9d95dd87c2f 100644 --- a/pkg/handlers/ghcapi/api.go +++ b/pkg/handlers/ghcapi/api.go @@ -525,6 +525,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 c6157c0377e..f6b4b4c5474 100644 --- a/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go +++ b/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go @@ -2152,6 +2152,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 e6fd7f18697..991371a99e2 100644 --- a/pkg/handlers/ghcapi/queues.go +++ b/pkg/handlers/ghcapi/queues.go @@ -538,6 +538,66 @@ func (h GetServicesCounselingQueueHandler) Handle( }) } +// GetServicesCounselingOriginListHandler returns the origin list for the Service Counselor user via GET /queues/counselor/origin-list +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) { + // fetch office user + 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 + } + // fetch their privileges + 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 + } + // confirm they have supervisor priv + 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) + } + 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 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 1857e10ed84..e39f3821968 100644 --- a/pkg/models/office_user.go +++ b/pkg/models/office_user.go @@ -43,6 +43,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 a0aecdab552..71d77747594 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/move.go b/pkg/services/move.go index 8a524accbe2..de5fbcc71cf 100644 --- a/pkg/services/move.go +++ b/pkg/services/move.go @@ -27,6 +27,11 @@ type MoveListFetcher interface { type MoveFetcher interface { FetchMove(appCtx appcontext.AppContext, locator string, searchParams *MoveFetcherParams) (*models.Move, error) FetchMovesForPPTASReports(appCtx appcontext.AppContext, params *MoveTaskOrderFetcherParams) (models.Moves, error) + // FetchMovesForBulkAssignmentCounseling(appCtx appcontext.AppContext, queueType *string, gbloc string, officeId uuid.UUID) ([]models.MoveWithEarliestDate, error) +} + +type MoveFetcherBulkAssignment interface { + FetchMovesForBulkAssignmentCounseling(appCtx appcontext.AppContext, gbloc string, officeId uuid.UUID) ([]models.MoveWithEarliestDate, error) } //go:generate mockery --name MoveSearcher diff --git a/pkg/services/move/move_fetcher.go b/pkg/services/move/move_fetcher.go index 470f4ca78a4..4300935a2bb 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,90 @@ func (f moveFetcher) FetchMovesForPPTASReports(appCtx appcontext.AppContext, par return moves, nil } + +type moveFetcherBulkAssignment struct { +} + +// NewMoveFetcher creates a new moveFetcher 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 +} + +// query := `SELECT m.id +// FROM moves as m +// JOIN +// ( +// SELECT + +// )` + +// query := appCtx.DB().Q(). +// InnerJoin("orders", "orders.id = moves.orders_id"). +// InnerJoin("service_members", "orders.service_member_id = service_members.id"). +// InnerJoin("mto_shipments", "mto_shipments.move_id = moves.id"). +// InnerJoin("duty_locations as origin_dl", "orders.origin_duty_location_id = origin_dl.id"). +// Where("moves.status = 'NEEDS SERVICE COUNSELING' and orders.gbloc = $1 AND moves.show = $2", gbloc, models.BoolPointer(true)) + +// if queueType == models.QueueTypeCounseling { +// query.LeftJoin("ppm_shipments", "ppm_shipments.shipment_id = mto_shipments.id"). +// Where("moves.status = 'NEEDS SERVICE COUNSELING' AND moves.sc_assigned_id IS NULL AND moves.counseling_transportation_office_id = $3", officeId). +// Where("(ppm_shipments.status IS NULL OR ppm_shipments.status NOT IN ($4, $5, $6))", models.PPMShipmentStatusWaitingOnCustomer, models.PPMShipmentStatusNeedsCloseout, models.PPMShipmentStatusCloseoutComplete) +// } + +// // err := query.GroupBy("moves.id").All(&moves) +// query.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"). +// GroupBy("moves.id"). +// Order("earliest_date ASC") +// err := query.All(&moves) diff --git a/pkg/services/move/move_fetcher_test.go b/pkg/services/move/move_fetcher_test.go index 9901f8a260e..1b0794a9093 100644 --- a/pkg/services/move/move_fetcher_test.go +++ b/pkg/services/move/move_fetcher_test.go @@ -1,11 +1,14 @@ package move import ( + "fmt" "time" "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 +105,320 @@ 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(), []factory.Customization{ + { + Model: models.TransportationOffice{ + ProvidesCloseout: true, + }, + }, + }, nil) + // requestedPickupDate := time.Date(2021, 04, 01, 0, 0, 0, 0, time.UTC) + + // 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.CloseoutOffice, + }, + // { + // Model: models.MTOShipment{ + // RequestedPickupDate: &requestedPickupDate, + // RequestedDeliveryDate: &requestedPickupDate, + // Status: models.MTOShipmentStatusSubmitted, + // }, + // }, + // { + // Model: models.PPMShipment{ + // Status: models.PPMShipmentStatusNeedsAdvanceApproval, + // }, + // }, + }, nil) + factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusNeedsServiceCounseling, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CloseoutOffice, + }, + // { + // Model: models.MTOShipment{ + // RequestedPickupDate: &requestedPickupDate, + // RequestedDeliveryDate: &requestedPickupDate, + // Status: models.MTOShipmentStatusSubmitted, + // }, + // }, + // { + // Model: models.PPMShipment{ + // Status: models.PPMShipmentStatusNeedsAdvanceApproval, + // }, + // }, + }, nil) + // factory.BuildMove(suite.DB(), nil, nil) + officeUser := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CloseoutOffice, + }, + }, []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(len(moves), 2) + }) + + suite.Run("Does not return moves that are counseled by a different counseling office", func() { + moveFetcher, _, _, officeUser := setupTestData() + transportationOffice := factory.BuildTransportationOffice(suite.DB(), []factory.Customization{ + { + Model: models.TransportationOffice{ + ProvidesCloseout: true, + }, + }, + }, nil) + + factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusNeedsServiceCounseling, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CloseoutOffice, + }, + }, nil) + + moves, err := moveFetcher.FetchMovesForBulkAssignmentCounseling(suite.AppContextForTest(), "KKFA", officeUser.TransportationOffice.ID) + suite.FatalNoError(err) + suite.Equal(len(moves), 2) + }) + + 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.CloseoutOffice, + }, + }, 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.CloseoutOffice, + }, + }, 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.CloseoutOffice, + }, + }, nil) + + moves, err := moveFetcher.FetchMovesForBulkAssignmentCounseling(suite.AppContextForTest(), "KKFA", officeUser.TransportationOffice.ID) + suite.FatalNoError(err) + suite.Equal(len(moves), 2) + }) + + suite.Run("Does not return moves with PPMs in waiting on customer, needs closeout, or closeout compelte status", func() { + moveFetcher, _, transportationOffice, officeUser := setupTestData() + move2 := factory.BuildMoveWithPPMShipment(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusNeedsServiceCounseling, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CloseoutOffice, + }, + { + Model: models.PPMShipment{ + Status: models.PPMShipmentStatusWaitingOnCustomer, + }, + }, + }, nil) + factory.BuildMoveWithPPMShipment(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusNeedsServiceCounseling, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CloseoutOffice, + }, + { + Model: models.PPMShipment{ + Status: models.PPMShipmentStatusNeedsCloseout, + }, + }, + }, nil) + fmt.Println("move2") + fmt.Println(len(move2.MTOShipments)) + fmt.Println(move2.ID) + // factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ + // { + // Model: models.Move{ + // Status: models.MoveStatusNeedsServiceCounseling, + // }, + // }, + // { + // Model: transportationOffice, + // LinkOnly: true, + // Type: &factory.TransportationOffices.CloseoutOffice, + // }, + // }, nil) + + moves, err := moveFetcher.FetchMovesForBulkAssignmentCounseling(suite.AppContextForTest(), "KKFA", officeUser.TransportationOffice.ID) + suite.FatalNoError(err) + suite.Equal(len(moves), 2) + for _, move := range moves { + fmt.Println("move.ID") + fmt.Println(move.ID) + } + }) + + suite.Run("Does not return moves that are already assigned", func() { + moveFetcher, _, transOffice, officeUser := setupTestData() + // transportationOffice := factory.BuildTransportationOffice(suite.DB(), []factory.Customization{ + // { + // Model: models.TransportationOffice{ + // ProvidesCloseout: true, + // }, + // }, + // }, nil) + + // assignedOfficeUser := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + // { + // Model: transOffice, + // LinkOnly: true, + // Type: &factory.TransportationOffices.CloseoutOffice, + // }, + // }, []roles.RoleType{roles.RoleTypeServicesCounselor}) + + factory.BuildAssignedMoveWithPPMShipment(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusNeedsServiceCounseling, + }, + }, + { + Model: transOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CloseoutOffice, + }, + }, nil) + + moves, err := moveFetcher.FetchMovesForBulkAssignmentCounseling(suite.AppContextForTest(), "KKFA", officeUser.TransportationOffice.ID) + suite.FatalNoError(err) + suite.Equal(len(moves), 2) + }) + + // moveFetcher := NewMoveFetcherBulkAssignment() + // transportationOffice := factory.BuildTransportationOffice(suite.DB(), []factory.Customization{ + // { + // Model: models.TransportationOffice{ + // ProvidesCloseout: true, + // }, + // }, + // }, nil) + // requestedPickupDate := time.Date(2021, 04, 01, 0, 0, 0, 0, time.UTC) + + // // this move has a transportation office associated with it that matches + // // the SC's transportation office and should be found + // factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ + // { + // Model: models.Move{ + // Status: models.MoveStatusNeedsServiceCounseling, + // }, + // }, + // { + // Model: transportationOffice, + // LinkOnly: true, + // Type: &factory.TransportationOffices.CloseoutOffice, + // }, + // { + // Model: models.MTOShipment{ + // RequestedPickupDate: &requestedPickupDate, + // RequestedDeliveryDate: &requestedPickupDate, + // Status: models.MTOShipmentStatusSubmitted, + // }, + // }, + // { + // Model: models.PPMShipment{ + // Status: models.PPMShipmentStatusNeedsAdvanceApproval, + // }, + // }, + // }, nil) + // officeUser := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + // { + // Model: transportationOffice, + // LinkOnly: true, + // Type: &factory.TransportationOffices.CloseoutOffice, + // }, + // }, []roles.RoleType{roles.RoleTypeServicesCounselor}) + + // moves, err := moveFetcher.FetchMovesForBulkAssignmentCounseling(suite.AppContextForTest(), "KKFA", officeUser.TransportationOffice.ID) + // suite.FatalNoError(err) + // suite.Equal(len(moves), 1) + // suite.Run("Returns moves that fulfill the queries criteria", func() { + // }) + +} diff --git a/pkg/services/office_user.go b/pkg/services/office_user.go index 4674a67a094..5d9d20b9c9f 100644 --- a/pkg/services/office_user.go +++ b/pkg/services/office_user.go @@ -24,6 +24,7 @@ type OfficeUserFetcherPop interface { FetchOfficeUserByID(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 45a95981c94..8dcc586c15e 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" @@ -115,6 +116,37 @@ 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 + 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/src/components/BulkAssignment/BulkAssignmentModal.jsx b/src/components/BulkAssignment/BulkAssignmentModal.jsx new file mode 100644 index 00000000000..93550198cc2 --- /dev/null +++ b/src/components/BulkAssignment/BulkAssignmentModal.jsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { Button } from '@trussworks/react-uswds'; + +import Modal, { ModalTitle, ModalClose, ModalActions, connectModal } from 'components/Modal/Modal'; +import { useBulkAssignmentQueries } from 'hooks/queries'; +import LoadingPlaceholder from 'shared/LoadingPlaceholder'; +import SomethingWentWrong from 'shared/SomethingWentWrong'; + +export const BulkAssignmentModal = ({ onSubmit, onClose, queueType }) => { + const { bulkAssignmentData, isLoading, isError } = useBulkAssignmentQueries(queueType); + if (isLoading) return ; + if (isError) return ; + return ( + + onClose()} /> + +

Bulk Assignment

+
+

Available moves: {bulkAssignmentData?.availableMoves}

+ {bulkAssignmentData?.availableOfficeUsers?.map((user) => { + return ( +
+ {`${user.lastName}, ${user.firstName} ||| workload: ${user.workload || 0}`} +
+ ); + })} + + + + +
+ ); +}; + +export default connectModal(BulkAssignmentModal); diff --git a/src/components/Table/TableQueue.jsx b/src/components/Table/TableQueue.jsx index 5114bde4bc5..c2710e76237 100644 --- a/src/components/Table/TableQueue.jsx +++ b/src/components/Table/TableQueue.jsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useMemo, useContext } from 'react'; -import { GridContainer } from '@trussworks/react-uswds'; +import { Button, GridContainer } from '@trussworks/react-uswds'; import { useTable, useFilters, usePagination, useSortBy } from 'react-table'; import PropTypes from 'prop-types'; @@ -10,7 +10,7 @@ import Table from 'components/Table/Table'; import LoadingPlaceholder from 'shared/LoadingPlaceholder'; import SomethingWentWrong from 'shared/SomethingWentWrong'; import TextBoxFilter from 'components/Table/Filters/TextBoxFilter'; -import { SortShape } from 'constants/queues'; +import { QUEUE_TYPES, SortShape } from 'constants/queues'; import SelectedGblocContext from 'components/Office/GblocSwitcher/SelectedGblocContext'; import { setTableQueueFilterSessionStorageValue, @@ -23,6 +23,7 @@ import { getTableQueueSortParamSessionStorageValue, getSelectionOptionLabel, } from 'components/Table/utils'; +import BulkAssignmentModal from 'components/BulkAssignment/BulkAssignmentModal'; const defaultPageSize = 20; const defaultPage = 1; @@ -51,6 +52,7 @@ const TableQueue = ({ isHeadquartersUser, }) => { const [isPageReload, setIsPageReload] = useState(true); + const [showBulkAssignmentModal, setShowBulkAssignmentModal] = useState(false); useEffect(() => { // Component is mounted. Set flag to tell component // subsequent effects are post mount. @@ -303,6 +305,24 @@ const TableQueue = ({

{`${title} (${totalCount})`}

+ {/* {showBulkAssignment && ( */} + setShowBulkAssignmentModal(false)} + onSubmit={() => console.log('ho')} + /> + + {/* )} */} {showCSVExport && ( { }; }; +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 58894e9eabd..93f75044dd8 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 57488fd4278..f97c2d82376 100644 --- a/swagger-def/ghc.yaml +++ b/swagger-def/ghc.yaml @@ -3470,6 +3470,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 relvant 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: @@ -6943,6 +6974,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: @@ -6958,6 +6997,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 466469e47f4..b4a80e607a4 100644 --- a/swagger/ghc.yaml +++ b/swagger/ghc.yaml @@ -3598,6 +3598,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 relvant 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: @@ -7268,6 +7302,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: @@ -7283,6 +7325,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: From 18a1a34193ceec00e3fb41d25fbbb0edc6175ae7 Mon Sep 17 00:00:00 2001 From: Paul Stonebraker Date: Wed, 11 Dec 2024 17:25:24 +0000 Subject: [PATCH 02/18] missed a file --- pkg/services/move/move_fetcher.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/services/move/move_fetcher.go b/pkg/services/move/move_fetcher.go index 4300935a2bb..d276ee3b193 100644 --- a/pkg/services/move/move_fetcher.go +++ b/pkg/services/move/move_fetcher.go @@ -123,20 +123,20 @@ func (f moveFetcherBulkAssignment) FetchMovesForBulkAssignmentCounseling(appCtx var moves []models.MoveWithEarliestDate err := appCtx.DB(). - RawQuery(`SELECT - moves.id, + 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(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 + 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 From 6a1af0dde4063048793a5dff04f6dd1c72986570 Mon Sep 17 00:00:00 2001 From: Paul Stonebraker Date: Fri, 13 Dec 2024 21:12:04 +0000 Subject: [PATCH 03/18] testing added, need cleanup --- pkg/factory/move_factory.go | 43 +-- pkg/factory/move_factory_test.go | 18 ++ pkg/factory/office_user_factory.go | 32 +- pkg/factory/office_user_factory_test.go | 50 +++ pkg/factory/shared.go | 8 + pkg/handlers/ghcapi/queues.go | 10 +- pkg/handlers/ghcapi/queues_test.go | 108 +++++++ pkg/services/move.go | 1 - pkg/services/move/move_fetcher_test.go | 305 ++++++++++++------ .../office_user/office_user_fetcher_test.go | 65 +++- 10 files changed, 493 insertions(+), 147 deletions(-) diff --git a/pkg/factory/move_factory.go b/pkg/factory/move_factory.go index 16233c288f7..3ac02f48e0a 100644 --- a/pkg/factory/move_factory.go +++ b/pkg/factory/move_factory.go @@ -1,7 +1,6 @@ package factory import ( - "fmt" "log" "time" @@ -36,6 +35,14 @@ func BuildMove(db *pop.Connection, customs []Customization, traits []Trait) mode closeoutOffice = BuildTransportationOffice(db, tempCloseoutOfficeCustoms, 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 { @@ -76,6 +83,11 @@ func BuildMove(db *pop.Connection, customs []Customization, traits []Trait) mode move.CounselingOfficeID = &closeoutOffice.ID } + if scAssignedUserResult != nil { + move.SCAssignedUser = &scAssignedUser + move.SCAssignedID = &scAssignedUser.ID + } + // Overwrite values with those from assertions testdatagen.MergeModels(&move, cMove) @@ -168,6 +180,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) @@ -184,34 +198,7 @@ func BuildMoveWithPPMShipment(db *pop.Connection, customs []Customization, trait if db != nil { mustSave(db, &move) } - fmt.Println("move in factory") - fmt.Println(move.ID) - - return move -} -func BuildAssignedMoveWithPPMShipment(db *pop.Connection, customs []Customization, traits []Trait) models.Move { - move := BuildMove(db, customs, traits) - - mtoShipment := buildMTOShipmentWithBuildType(db, customs, traits, mtoShipmentPPM) - mtoShipment.MoveTaskOrder = move - mtoShipment.MoveTaskOrderID = move.ID - ppmShipment := BuildPPMShipment(db, customs, traits) - ppmShipment.ShipmentID = mtoShipment.ID - - mtoShipment.PPMShipment = &ppmShipment - mtoShipment.ShipmentType = models.MTOShipmentTypePPM - move.MTOShipments = append(move.MTOShipments, mtoShipment) - - officeUser := BuildOfficeUser(db, customs, traits) - move.SCAssignedUser = &officeUser - move.SCAssignedID = &officeUser.ID - - if db != nil { - mustSave(db, &move) - } - fmt.Println("move") - fmt.Println(move.SCAssignedUser) return move } 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 fbd9b2cc784..36bd6d9a603 100644 --- a/pkg/factory/office_user_factory.go +++ b/pkg/factory/office_user_factory.go @@ -148,18 +148,32 @@ func BuildOfficeUserWithPrivileges(db *pop.Connection, customs []Customization, user := BuildUserAndUsersRolesAndUsersPrivileges(db, customs, nil) // Find/create the TransportationOffice model - transportationOffice := BuildTransportationOffice(db, customs, nil) + basicTransportationOffice := BuildTransportationOffice(db, customs, nil) + + var closeoutOffice models.TransportationOffice + tempCloseoutOfficeCustoms := customs + closeoutOfficeResult := findValidCustomization(customs, TransportationOffices.CloseoutOffice) + if closeoutOfficeResult != nil { + tempCloseoutOfficeCustoms = convertCustomizationInList(tempCloseoutOfficeCustoms, TransportationOffices.CloseoutOffice, TransportationOffice) + closeoutOffice = BuildTransportationOffice(db, tempCloseoutOfficeCustoms, nil) + } // create officeuser officeUser := models.OfficeUser{ - UserID: &user.ID, - User: user, - FirstName: "Leo", - LastName: "Spaceman", - Email: "leo_spaceman_office@example.com", - Telephone: "415-555-1212", - TransportationOffice: transportationOffice, - TransportationOfficeID: transportationOffice.ID, + UserID: &user.ID, + User: user, + FirstName: "Leo", + LastName: "Spaceman", + Email: "leo_spaceman_office@example.com", + Telephone: "415-555-1212", + } + + if closeoutOfficeResult != nil { + officeUser.TransportationOffice = closeoutOffice + officeUser.TransportationOfficeID = closeoutOffice.ID + } else { + officeUser.TransportationOffice = basicTransportationOffice + officeUser.TransportationOfficeID = basicTransportationOffice.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 a6eca09823a..27b8203ca59 100644 --- a/pkg/factory/shared.go +++ b/pkg/factory/shared.go @@ -263,6 +263,14 @@ var TransportationOffices = transportationOfficeGroup{ CounselingOffice: "CounselingOffice", } +type officeUserGroup struct { + SCAssignedUser CustomType +} + +var OfficeUsers = officeUserGroup{ + SCAssignedUser: "SCAssignedUser", +} + // uploadGroup is a grouping of all the upload related fields type uploadGroup struct { UploadTypePrime CustomType diff --git a/pkg/handlers/ghcapi/queues.go b/pkg/handlers/ghcapi/queues.go index 991371a99e2..cc6b53b5565 100644 --- a/pkg/handlers/ghcapi/queues.go +++ b/pkg/handlers/ghcapi/queues.go @@ -550,24 +550,24 @@ func (h GetBulkAssignmentDataHandler) Handle( ) middleware.Responder { return h.AuditableAppContextFromRequestWithErrors(params.HTTPRequest, func(appCtx appcontext.AppContext) (middleware.Responder, error) { - // fetch office user 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 } - // fetch their privileges + 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 } - // confirm they have supervisor priv + 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 @@ -591,7 +591,8 @@ func (h GetBulkAssignmentDataHandler) Handle( appCtx.Logger().Error("Error retreiving moves", zap.Error(err)) return queues.NewGetBulkAssignmentDataInternalServerError(), err } - + fmt.Println("moves") + fmt.Println(moves) officeUserData = payloads.BulkAssignmentData(appCtx, moves, officeUsers, officeUser.TransportationOffice.ID) } return queues.NewGetBulkAssignmentDataOK().WithPayload(&officeUserData), nil @@ -622,7 +623,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..904744adc9e 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,110 @@ 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(), []factory.Customization{ + { + Model: models.TransportationOffice{ + ProvidesCloseout: true, + }, + }, + }, nil) + + officeUser := factory.BuildOfficeUserWithPrivileges(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + Email: "officeuser1@example.com", + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CloseoutOffice, + }, + { + 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.CloseoutOffice, + }, + }, 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) + }) +} diff --git a/pkg/services/move.go b/pkg/services/move.go index de5fbcc71cf..237e360fe3f 100644 --- a/pkg/services/move.go +++ b/pkg/services/move.go @@ -27,7 +27,6 @@ type MoveListFetcher interface { type MoveFetcher interface { FetchMove(appCtx appcontext.AppContext, locator string, searchParams *MoveFetcherParams) (*models.Move, error) FetchMovesForPPTASReports(appCtx appcontext.AppContext, params *MoveTaskOrderFetcherParams) (models.Moves, error) - // FetchMovesForBulkAssignmentCounseling(appCtx appcontext.AppContext, queueType *string, gbloc string, officeId uuid.UUID) ([]models.MoveWithEarliestDate, error) } type MoveFetcherBulkAssignment interface { diff --git a/pkg/services/move/move_fetcher_test.go b/pkg/services/move/move_fetcher_test.go index 1b0794a9093..ec8bd373e98 100644 --- a/pkg/services/move/move_fetcher_test.go +++ b/pkg/services/move/move_fetcher_test.go @@ -1,7 +1,6 @@ package move import ( - "fmt" "time" "github.com/transcom/mymove/pkg/apperror" @@ -274,14 +273,18 @@ func (suite *MoveServiceSuite) TestMoveFetcherBulkAssignment() { suite.Equal(len(moves), 2) }) + // // 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, needs closeout, or closeout compelte status", func() { - moveFetcher, _, transportationOffice, officeUser := setupTestData() - move2 := factory.BuildMoveWithPPMShipment(suite.DB(), []factory.Customization{ + moveFetcher := NewMoveFetcherBulkAssignment() + transportationOffice := factory.BuildTransportationOffice(suite.DB(), []factory.Customization{ { - Model: models.Move{ - Status: models.MoveStatusNeedsServiceCounseling, + Model: models.TransportationOffice{ + ProvidesCloseout: true, }, }, + }, nil) + moveWithPPM := factory.BuildMoveWithPPMShipment(suite.DB(), []factory.Customization{ { Model: transportationOffice, LinkOnly: true, @@ -292,13 +295,47 @@ func (suite *MoveServiceSuite) TestMoveFetcherBulkAssignment() { Status: models.PPMShipmentStatusWaitingOnCustomer, }, }, - }, nil) - factory.BuildMoveWithPPMShipment(suite.DB(), []factory.Customization{ + }, []factory.Trait{factory.GetTraitNeedsServiceCounselingMove}) + + officeUser := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ { - Model: models.Move{ - Status: models.MoveStatusNeedsServiceCounseling, + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CloseoutOffice, + }, + }, []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, moveWithPPM.ID) + // confirm that the rest of the details are correct + // move is NEEDS SERVICE COUNSELING STATUS + suite.Equal(moveWithPPM.Status, models.MoveStatusNeedsServiceCounseling) + // move is not assigned to anyone + suite.Nil(moveWithPPM.SCAssignedID) + // GBLOC is the same + suite.Equal(*moveWithPPM.Orders.OriginDutyLocationGBLOC, officeUser.TransportationOffice.Gbloc) + // Show is true + suite.Equal(moveWithPPM.Show, models.BoolPointer(true)) + // Move is counseled by the office user's office + suite.Equal(*moveWithPPM.CounselingOfficeID, officeUser.TransportationOfficeID) + // Orders type isn't WW, BB, or Safety + suite.Equal(moveWithPPM.Orders.OrdersType, internalmessages.OrdersTypePERMANENTCHANGEOFSTATION) + }) + + suite.Run("Does not return moves with PPMs in waiting on customer, needs closeout, or closeout compelte status", func() { + moveFetcher := NewMoveFetcherBulkAssignment() + transportationOffice := factory.BuildTransportationOffice(suite.DB(), []factory.Customization{ + { + Model: models.TransportationOffice{ + ProvidesCloseout: true, }, }, + }, nil) + moveWithPPM := factory.BuildMoveWithPPMShipment(suite.DB(), []factory.Customization{ { Model: transportationOffice, LinkOnly: true, @@ -309,116 +346,194 @@ func (suite *MoveServiceSuite) TestMoveFetcherBulkAssignment() { Status: models.PPMShipmentStatusNeedsCloseout, }, }, + }, []factory.Trait{factory.GetTraitNeedsServiceCounselingMove}) + + officeUser := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CloseoutOffice, + }, + }, []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, moveWithPPM.ID) + // confirm that the rest of the details are correct + // move is NEEDS SERVICE COUNSELING STATUS + suite.Equal(moveWithPPM.Status, models.MoveStatusNeedsServiceCounseling) + // move is not assigned to anyone + suite.Nil(moveWithPPM.SCAssignedID) + // GBLOC is the same + suite.Equal(*moveWithPPM.Orders.OriginDutyLocationGBLOC, officeUser.TransportationOffice.Gbloc) + // Show is true + suite.Equal(moveWithPPM.Show, models.BoolPointer(true)) + // Move is counseled by the office user's office + suite.Equal(*moveWithPPM.CounselingOfficeID, officeUser.TransportationOfficeID) + // Orders type isn't WW, BB, or Safety + suite.Equal(moveWithPPM.Orders.OrdersType, internalmessages.OrdersTypePERMANENTCHANGEOFSTATION) + }) + suite.Run("Does not return moves with PPMs in waiting on customer, needs closeout, or closeout compelte status", func() { + moveFetcher := NewMoveFetcherBulkAssignment() + transportationOffice := factory.BuildTransportationOffice(suite.DB(), []factory.Customization{ + { + Model: models.TransportationOffice{ + ProvidesCloseout: true, + }, + }, }, nil) - fmt.Println("move2") - fmt.Println(len(move2.MTOShipments)) - fmt.Println(move2.ID) - // factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ - // { - // Model: models.Move{ - // Status: models.MoveStatusNeedsServiceCounseling, - // }, - // }, - // { - // Model: transportationOffice, - // LinkOnly: true, - // Type: &factory.TransportationOffices.CloseoutOffice, - // }, - // }, nil) + moveWithPPM := factory.BuildMoveWithPPMShipment(suite.DB(), []factory.Customization{ + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CloseoutOffice, + }, + { + Model: models.PPMShipment{ + Status: models.PPMShipmentStatusCloseoutComplete, + }, + }, + }, []factory.Trait{factory.GetTraitNeedsServiceCounselingMove}) + + officeUser := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CloseoutOffice, + }, + }, []roles.RoleType{roles.RoleTypeServicesCounselor}) moves, err := moveFetcher.FetchMovesForBulkAssignmentCounseling(suite.AppContextForTest(), "KKFA", officeUser.TransportationOffice.ID) suite.FatalNoError(err) - suite.Equal(len(moves), 2) - for _, move := range moves { - fmt.Println("move.ID") - fmt.Println(move.ID) - } + // 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, moveWithPPM.ID) + // confirm that the rest of the details are correct + // move is NEEDS SERVICE COUNSELING STATUS + suite.Equal(moveWithPPM.Status, models.MoveStatusNeedsServiceCounseling) + // move is not assigned to anyone + suite.Nil(moveWithPPM.SCAssignedID) + // GBLOC is the same + suite.Equal(*moveWithPPM.Orders.OriginDutyLocationGBLOC, officeUser.TransportationOffice.Gbloc) + // Show is true + suite.Equal(moveWithPPM.Show, models.BoolPointer(true)) + // Move is counseled by the office user's office + suite.Equal(*moveWithPPM.CounselingOfficeID, officeUser.TransportationOfficeID) + // Orders type isn't WW, BB, or Safety + suite.Equal(moveWithPPM.Orders.OrdersType, internalmessages.OrdersTypePERMANENTCHANGEOFSTATION) }) suite.Run("Does not return moves that are already assigned", func() { - moveFetcher, _, transOffice, officeUser := setupTestData() - // transportationOffice := factory.BuildTransportationOffice(suite.DB(), []factory.Customization{ - // { - // Model: models.TransportationOffice{ - // ProvidesCloseout: true, - // }, - // }, - // }, nil) - - // assignedOfficeUser := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ - // { - // Model: transOffice, - // LinkOnly: true, - // Type: &factory.TransportationOffices.CloseoutOffice, - // }, - // }, []roles.RoleType{roles.RoleTypeServicesCounselor}) - - factory.BuildAssignedMoveWithPPMShipment(suite.DB(), []factory.Customization{ + // moveFetcher, _, transOffice, officeUser := setupTestData() + moveFetcher := NewMoveFetcherBulkAssignment() + transportationOffice := factory.BuildTransportationOffice(suite.DB(), []factory.Customization{ + { + Model: models.TransportationOffice{ + ProvidesCloseout: true, + }, + }, + }, nil) + + officeUser := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CloseoutOffice, + }, + }, []roles.RoleType{roles.RoleTypeServicesCounselor}) + + assignedMove := factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ { Model: models.Move{ Status: models.MoveStatusNeedsServiceCounseling, }, }, { - Model: transOffice, + Model: transportationOffice, LinkOnly: true, Type: &factory.TransportationOffices.CloseoutOffice, }, + { + Model: officeUser, + LinkOnly: true, + Type: &factory.OfficeUsers.SCAssignedUser, + }, }, nil) moves, err := moveFetcher.FetchMovesForBulkAssignmentCounseling(suite.AppContextForTest(), "KKFA", officeUser.TransportationOffice.ID) suite.FatalNoError(err) - suite.Equal(len(moves), 2) - }) - // moveFetcher := NewMoveFetcherBulkAssignment() - // transportationOffice := factory.BuildTransportationOffice(suite.DB(), []factory.Customization{ - // { - // Model: models.TransportationOffice{ - // ProvidesCloseout: true, + // 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("Does not return moves that are already assigned", func() { + // // moveFetcher, _, transOffice, officeUser := setupTestData() + // moveFetcher := NewMoveFetcherBulkAssignment() + // transportationOffice := factory.BuildTransportationOffice(suite.DB(), []factory.Customization{ + // { + // Model: models.TransportationOffice{ + // ProvidesCloseout: true, + // }, // }, - // }, - // }, nil) - // requestedPickupDate := time.Date(2021, 04, 01, 0, 0, 0, 0, time.UTC) - - // // this move has a transportation office associated with it that matches - // // the SC's transportation office and should be found - // factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ - // { - // Model: models.Move{ - // Status: models.MoveStatusNeedsServiceCounseling, + // }, nil) + + // officeUser := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + // { + // Model: transportationOffice, + // LinkOnly: true, + // Type: &factory.TransportationOffices.CloseoutOffice, // }, - // }, - // { - // Model: transportationOffice, - // LinkOnly: true, - // Type: &factory.TransportationOffices.CloseoutOffice, - // }, - // { - // Model: models.MTOShipment{ - // RequestedPickupDate: &requestedPickupDate, - // RequestedDeliveryDate: &requestedPickupDate, - // Status: models.MTOShipmentStatusSubmitted, + // }, []roles.RoleType{roles.RoleTypeServicesCounselor}) + + // assignedMove, _ := factory.BuildSCAssignedMoveWithPPMShipment(suite.DB(), []factory.Customization{ + // { + // Model: models.Move{ + // Status: models.MoveStatusNeedsServiceCounseling, + // }, // }, - // }, - // { - // Model: models.PPMShipment{ - // Status: models.PPMShipmentStatusNeedsAdvanceApproval, + // { + // Model: transportationOffice, + // LinkOnly: true, + // Type: &factory.TransportationOffices.CloseoutOffice, // }, - // }, - // }, nil) - // officeUser := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ - // { - // Model: transportationOffice, - // LinkOnly: true, - // Type: &factory.TransportationOffices.CloseoutOffice, - // }, - // }, []roles.RoleType{roles.RoleTypeServicesCounselor}) - - // moves, err := moveFetcher.FetchMovesForBulkAssignmentCounseling(suite.AppContextForTest(), "KKFA", officeUser.TransportationOffice.ID) - // suite.FatalNoError(err) - // suite.Equal(len(moves), 1) - // suite.Run("Returns moves that fulfill the queries criteria", func() { + // }, 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) // }) - } diff --git a/pkg/services/office_user/office_user_fetcher_test.go b/pkg/services/office_user/office_user_fetcher_test.go index 5ce6c1aeda4..f409359a7be 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,18 @@ 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) TestFetchOfficeUsersByRoleAndOffice() { + 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 @@ -112,13 +123,49 @@ func (suite *OfficeUserServiceSuite) TestFetchOfficeUserPop() { suite.NoError(err) suite.Len(fetchedUsers, 1) }) +} - suite.Run("returns zero value office user on error", func() { +func (suite *OfficeUserServiceSuite) TestFetchOfficeUsersWithWorkloadByRoleAndOffice() { + suite.Run("FetchOfficeUsersWithWorkloadByRoleAndOffice returns an office user's name, id, and workload when given a role and office", 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) + transportationOffice := factory.BuildTransportationOffice(suite.DB(), []factory.Customization{ + { + Model: models.TransportationOffice{ + ProvidesCloseout: true, + }, + }, + }, nil) + + officeUser := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CloseoutOffice, + }, + }, []roles.RoleType{roles.RoleTypeServicesCounselor}) + + factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusNeedsServiceCounseling, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CloseoutOffice, + }, + { + 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) }) } From 2108808e7afffdfc0b0ebaecbdc4592f5aed13da Mon Sep 17 00:00:00 2001 From: Paul Stonebraker Date: Tue, 17 Dec 2024 05:36:51 +0000 Subject: [PATCH 04/18] clean up and update tests --- pkg/factory/move_factory.go | 3 - pkg/factory/office_user_factory.go | 3 +- pkg/gen/ghcapi/embedded_spec.go | 4 +- .../queues/get_bulk_assignment_data.go | 2 +- pkg/handlers/ghcapi/queues.go | 8 +- pkg/services/move/move_fetcher.go | 34 +------ pkg/services/move/move_fetcher_test.go | 92 ++----------------- swagger-def/ghc.yaml | 2 +- swagger/ghc.yaml | 2 +- 9 files changed, 22 insertions(+), 128 deletions(-) diff --git a/pkg/factory/move_factory.go b/pkg/factory/move_factory.go index 3ac02f48e0a..26f52cd17f6 100644 --- a/pkg/factory/move_factory.go +++ b/pkg/factory/move_factory.go @@ -77,14 +77,11 @@ func BuildMove(db *pop.Connection, customs []Customization, traits []Trait) mode } if closeoutOfficeResult != nil { - move.CloseoutOffice = &closeoutOffice move.CloseoutOfficeID = &closeoutOffice.ID - move.CounselingOffice = &closeoutOffice move.CounselingOfficeID = &closeoutOffice.ID } if scAssignedUserResult != nil { - move.SCAssignedUser = &scAssignedUser move.SCAssignedID = &scAssignedUser.ID } diff --git a/pkg/factory/office_user_factory.go b/pkg/factory/office_user_factory.go index 36bd6d9a603..8026fd92d87 100644 --- a/pkg/factory/office_user_factory.go +++ b/pkg/factory/office_user_factory.go @@ -55,10 +55,9 @@ func BuildOfficeUser(db *pop.Connection, customs []Customization, traits []Trait LastName: "Spaceman", Email: "leo_spaceman_office@example.com", Telephone: "415-555-1212", - // TransportationOffice: transportationOffice, - // TransportationOfficeID: transportationOffice.ID, } + // DAD TODO: remove model or ID? if closeoutOfficeResult != nil { officeUser.TransportationOffice = closeoutOffice officeUser.TransportationOfficeID = closeoutOffice.ID diff --git a/pkg/gen/ghcapi/embedded_spec.go b/pkg/gen/ghcapi/embedded_spec.go index f4d48668f5f..d1eb6f801b1 100644 --- a/pkg/gen/ghcapi/embedded_spec.go +++ b/pkg/gen/ghcapi/embedded_spec.go @@ -4276,7 +4276,7 @@ func init() { }, "/queues/bulk-assignment": { "get": { - "description": "Supervisor office users are able to bulk assign moves. This endpoint returns the relvant data to them; the current workload of the office users that work under them, and the moves that are available to be assigned\n", + "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" ], @@ -20433,7 +20433,7 @@ func init() { }, "/queues/bulk-assignment": { "get": { - "description": "Supervisor office users are able to bulk assign moves. This endpoint returns the relvant data to them; the current workload of the office users that work under them, and the moves that are available to be assigned\n", + "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" ], diff --git a/pkg/gen/ghcapi/ghcoperations/queues/get_bulk_assignment_data.go b/pkg/gen/ghcapi/ghcoperations/queues/get_bulk_assignment_data.go index e9ffdc1dc42..a48fbcedc45 100644 --- a/pkg/gen/ghcapi/ghcoperations/queues/get_bulk_assignment_data.go +++ b/pkg/gen/ghcapi/ghcoperations/queues/get_bulk_assignment_data.go @@ -34,7 +34,7 @@ func NewGetBulkAssignmentData(ctx *middleware.Context, handler GetBulkAssignment # Gets data for bulk assignment modal -Supervisor office users are able to bulk assign moves. This endpoint returns the relvant data to them; the current workload of the office users that work under them, and the moves that are available to be assigned +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 diff --git a/pkg/handlers/ghcapi/queues.go b/pkg/handlers/ghcapi/queues.go index cc6b53b5565..b7b2cdcb8b6 100644 --- a/pkg/handlers/ghcapi/queues.go +++ b/pkg/handlers/ghcapi/queues.go @@ -538,7 +538,7 @@ func (h GetServicesCounselingQueueHandler) Handle( }) } -// GetServicesCounselingOriginListHandler returns the origin list for the Service Counselor user via GET /queues/counselor/origin-list +// 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 @@ -550,6 +550,12 @@ func (h GetBulkAssignmentDataHandler) Handle( ) 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)) diff --git a/pkg/services/move/move_fetcher.go b/pkg/services/move/move_fetcher.go index d276ee3b193..f4fc42dea57 100644 --- a/pkg/services/move/move_fetcher.go +++ b/pkg/services/move/move_fetcher.go @@ -114,7 +114,7 @@ func (f moveFetcher) FetchMovesForPPTASReports(appCtx appcontext.AppContext, par type moveFetcherBulkAssignment struct { } -// NewMoveFetcher creates a new moveFetcher service +// NewMoveFetcherBulkAssignment creates a new moveFetcherBulkAssignment service func NewMoveFetcherBulkAssignment() services.MoveFetcherBulkAssignment { return &moveFetcherBulkAssignment{} } @@ -165,35 +165,3 @@ func (f moveFetcherBulkAssignment) FetchMovesForBulkAssignmentCounseling(appCtx return moves, nil } - -// query := `SELECT m.id -// FROM moves as m -// JOIN -// ( -// SELECT - -// )` - -// query := appCtx.DB().Q(). -// InnerJoin("orders", "orders.id = moves.orders_id"). -// InnerJoin("service_members", "orders.service_member_id = service_members.id"). -// InnerJoin("mto_shipments", "mto_shipments.move_id = moves.id"). -// InnerJoin("duty_locations as origin_dl", "orders.origin_duty_location_id = origin_dl.id"). -// Where("moves.status = 'NEEDS SERVICE COUNSELING' and orders.gbloc = $1 AND moves.show = $2", gbloc, models.BoolPointer(true)) - -// if queueType == models.QueueTypeCounseling { -// query.LeftJoin("ppm_shipments", "ppm_shipments.shipment_id = mto_shipments.id"). -// Where("moves.status = 'NEEDS SERVICE COUNSELING' AND moves.sc_assigned_id IS NULL AND moves.counseling_transportation_office_id = $3", officeId). -// Where("(ppm_shipments.status IS NULL OR ppm_shipments.status NOT IN ($4, $5, $6))", models.PPMShipmentStatusWaitingOnCustomer, models.PPMShipmentStatusNeedsCloseout, models.PPMShipmentStatusCloseoutComplete) -// } - -// // err := query.GroupBy("moves.id").All(&moves) -// query.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"). -// GroupBy("moves.id"). -// Order("earliest_date ASC") -// err := query.All(&moves) diff --git a/pkg/services/move/move_fetcher_test.go b/pkg/services/move/move_fetcher_test.go index ec8bd373e98..7a0dab1d277 100644 --- a/pkg/services/move/move_fetcher_test.go +++ b/pkg/services/move/move_fetcher_test.go @@ -115,7 +115,6 @@ func (suite *MoveServiceSuite) TestMoveFetcherBulkAssignment() { }, }, }, nil) - // requestedPickupDate := time.Date(2021, 04, 01, 0, 0, 0, 0, time.UTC) // this move has a transportation office associated with it that matches // the SC's transportation office and should be found @@ -130,19 +129,8 @@ func (suite *MoveServiceSuite) TestMoveFetcherBulkAssignment() { LinkOnly: true, Type: &factory.TransportationOffices.CloseoutOffice, }, - // { - // Model: models.MTOShipment{ - // RequestedPickupDate: &requestedPickupDate, - // RequestedDeliveryDate: &requestedPickupDate, - // Status: models.MTOShipmentStatusSubmitted, - // }, - // }, - // { - // Model: models.PPMShipment{ - // Status: models.PPMShipmentStatusNeedsAdvanceApproval, - // }, - // }, }, nil) + factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ { Model: models.Move{ @@ -154,20 +142,8 @@ func (suite *MoveServiceSuite) TestMoveFetcherBulkAssignment() { LinkOnly: true, Type: &factory.TransportationOffices.CloseoutOffice, }, - // { - // Model: models.MTOShipment{ - // RequestedPickupDate: &requestedPickupDate, - // RequestedDeliveryDate: &requestedPickupDate, - // Status: models.MTOShipmentStatusSubmitted, - // }, - // }, - // { - // Model: models.PPMShipment{ - // Status: models.PPMShipmentStatusNeedsAdvanceApproval, - // }, - // }, }, nil) - // factory.BuildMove(suite.DB(), nil, nil) + officeUser := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ { Model: transportationOffice, @@ -183,7 +159,7 @@ func (suite *MoveServiceSuite) TestMoveFetcherBulkAssignment() { moveFetcher, _, _, officeUser := setupTestData() moves, err := moveFetcher.FetchMovesForBulkAssignmentCounseling(suite.AppContextForTest(), "KKFA", officeUser.TransportationOffice.ID) suite.FatalNoError(err) - suite.Equal(len(moves), 2) + suite.Equal(2, len(moves)) }) suite.Run("Does not return moves that are counseled by a different counseling office", func() { @@ -273,9 +249,9 @@ func (suite *MoveServiceSuite) TestMoveFetcherBulkAssignment() { suite.Equal(len(moves), 2) }) - // // 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, needs closeout, or closeout compelte status", func() { + // 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(), []factory.Customization{ { @@ -326,7 +302,7 @@ func (suite *MoveServiceSuite) TestMoveFetcherBulkAssignment() { suite.Equal(moveWithPPM.Orders.OrdersType, internalmessages.OrdersTypePERMANENTCHANGEOFSTATION) }) - suite.Run("Does not return moves with PPMs in waiting on customer, needs closeout, or closeout compelte status", func() { + suite.Run("Does not return moves with PPMs in wneeds closeout", func() { moveFetcher := NewMoveFetcherBulkAssignment() transportationOffice := factory.BuildTransportationOffice(suite.DB(), []factory.Customization{ { @@ -376,7 +352,7 @@ func (suite *MoveServiceSuite) TestMoveFetcherBulkAssignment() { // Orders type isn't WW, BB, or Safety suite.Equal(moveWithPPM.Orders.OrdersType, internalmessages.OrdersTypePERMANENTCHANGEOFSTATION) }) - suite.Run("Does not return moves with PPMs in waiting on customer, needs closeout, or closeout compelte status", func() { + suite.Run("Does not return moves with PPMs in closeout complete status", func() { moveFetcher := NewMoveFetcherBulkAssignment() transportationOffice := factory.BuildTransportationOffice(suite.DB(), []factory.Customization{ { @@ -484,56 +460,4 @@ func (suite *MoveServiceSuite) TestMoveFetcherBulkAssignment() { // Orders type isn't WW, BB, or Safety suite.Equal(assignedMove.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(), []factory.Customization{ - // { - // Model: models.TransportationOffice{ - // ProvidesCloseout: true, - // }, - // }, - // }, nil) - - // officeUser := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ - // { - // Model: transportationOffice, - // LinkOnly: true, - // Type: &factory.TransportationOffices.CloseoutOffice, - // }, - // }, []roles.RoleType{roles.RoleTypeServicesCounselor}) - - // assignedMove, _ := factory.BuildSCAssignedMoveWithPPMShipment(suite.DB(), []factory.Customization{ - // { - // Model: models.Move{ - // Status: models.MoveStatusNeedsServiceCounseling, - // }, - // }, - // { - // Model: transportationOffice, - // LinkOnly: true, - // Type: &factory.TransportationOffices.CloseoutOffice, - // }, - // }, 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) - // }) } diff --git a/swagger-def/ghc.yaml b/swagger-def/ghc.yaml index f97c2d82376..cc5ff46d602 100644 --- a/swagger-def/ghc.yaml +++ b/swagger-def/ghc.yaml @@ -3476,7 +3476,7 @@ paths: - application/json summary: Gets data for bulk assignment modal description: > - Supervisor office users are able to bulk assign moves. This endpoint returns the relvant data to them; the current workload of the office users that work under them, and the moves that are available to be assigned + 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 diff --git a/swagger/ghc.yaml b/swagger/ghc.yaml index b4a80e607a4..136efee2e9d 100644 --- a/swagger/ghc.yaml +++ b/swagger/ghc.yaml @@ -3605,7 +3605,7 @@ paths: summary: Gets data for bulk assignment modal description: > Supervisor office users are able to bulk assign moves. This endpoint - returns the relvant data to them; the current workload of the office + 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 From 06a5f3c884a20bf7727666ff5c2d64528527c40d Mon Sep 17 00:00:00 2001 From: Paul Stonebraker Date: Tue, 17 Dec 2024 06:11:07 +0000 Subject: [PATCH 05/18] remove front end mock ups --- .../BulkAssignment/BulkAssignmentModal.jsx | 45 ------------------- src/components/Table/TableQueue.jsx | 24 +--------- 2 files changed, 2 insertions(+), 67 deletions(-) delete mode 100644 src/components/BulkAssignment/BulkAssignmentModal.jsx diff --git a/src/components/BulkAssignment/BulkAssignmentModal.jsx b/src/components/BulkAssignment/BulkAssignmentModal.jsx deleted file mode 100644 index 93550198cc2..00000000000 --- a/src/components/BulkAssignment/BulkAssignmentModal.jsx +++ /dev/null @@ -1,45 +0,0 @@ -import React from 'react'; -import { Button } from '@trussworks/react-uswds'; - -import Modal, { ModalTitle, ModalClose, ModalActions, connectModal } from 'components/Modal/Modal'; -import { useBulkAssignmentQueries } from 'hooks/queries'; -import LoadingPlaceholder from 'shared/LoadingPlaceholder'; -import SomethingWentWrong from 'shared/SomethingWentWrong'; - -export const BulkAssignmentModal = ({ onSubmit, onClose, queueType }) => { - const { bulkAssignmentData, isLoading, isError } = useBulkAssignmentQueries(queueType); - if (isLoading) return ; - if (isError) return ; - return ( - - onClose()} /> - -

Bulk Assignment

-
-

Available moves: {bulkAssignmentData?.availableMoves}

- {bulkAssignmentData?.availableOfficeUsers?.map((user) => { - return ( -
- {`${user.lastName}, ${user.firstName} ||| workload: ${user.workload || 0}`} -
- ); - })} - - - - -
- ); -}; - -export default connectModal(BulkAssignmentModal); diff --git a/src/components/Table/TableQueue.jsx b/src/components/Table/TableQueue.jsx index c2710e76237..5114bde4bc5 100644 --- a/src/components/Table/TableQueue.jsx +++ b/src/components/Table/TableQueue.jsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useMemo, useContext } from 'react'; -import { Button, GridContainer } from '@trussworks/react-uswds'; +import { GridContainer } from '@trussworks/react-uswds'; import { useTable, useFilters, usePagination, useSortBy } from 'react-table'; import PropTypes from 'prop-types'; @@ -10,7 +10,7 @@ import Table from 'components/Table/Table'; import LoadingPlaceholder from 'shared/LoadingPlaceholder'; import SomethingWentWrong from 'shared/SomethingWentWrong'; import TextBoxFilter from 'components/Table/Filters/TextBoxFilter'; -import { QUEUE_TYPES, SortShape } from 'constants/queues'; +import { SortShape } from 'constants/queues'; import SelectedGblocContext from 'components/Office/GblocSwitcher/SelectedGblocContext'; import { setTableQueueFilterSessionStorageValue, @@ -23,7 +23,6 @@ import { getTableQueueSortParamSessionStorageValue, getSelectionOptionLabel, } from 'components/Table/utils'; -import BulkAssignmentModal from 'components/BulkAssignment/BulkAssignmentModal'; const defaultPageSize = 20; const defaultPage = 1; @@ -52,7 +51,6 @@ const TableQueue = ({ isHeadquartersUser, }) => { const [isPageReload, setIsPageReload] = useState(true); - const [showBulkAssignmentModal, setShowBulkAssignmentModal] = useState(false); useEffect(() => { // Component is mounted. Set flag to tell component // subsequent effects are post mount. @@ -305,24 +303,6 @@ const TableQueue = ({

{`${title} (${totalCount})`}

- {/* {showBulkAssignment && ( */} - setShowBulkAssignmentModal(false)} - onSubmit={() => console.log('ho')} - /> - - {/* )} */} {showCSVExport && ( Date: Tue, 17 Dec 2024 13:33:49 +0000 Subject: [PATCH 06/18] correct queueu type typo --- src/constants/queues.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/constants/queues.js b/src/constants/queues.js index 181fffefd4c..1ee59cd111d 100644 --- a/src/constants/queues.js +++ b/src/constants/queues.js @@ -91,7 +91,7 @@ export const GBLOC = { export const QUEUE_TYPES = { COUNSELING: 'COUNSELING', CLOSEOUT: 'CLOSEOUT', - TASK_ORDERING: 'TASK_ORDERING', + TASK_ORDER: 'TASK_ORDER', PAYMENT_REQUEST: 'PAYMENT_REQUEST', }; From f0bd05692e1dfbb043ef0049bd93fd8ef4af2043 Mon Sep 17 00:00:00 2001 From: Paul Stonebraker Date: Tue, 17 Dec 2024 14:28:16 +0000 Subject: [PATCH 07/18] update office user factory after merge --- pkg/factory/office_user_factory.go | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/pkg/factory/office_user_factory.go b/pkg/factory/office_user_factory.go index 3d299262952..ac0c82284f0 100644 --- a/pkg/factory/office_user_factory.go +++ b/pkg/factory/office_user_factory.go @@ -37,34 +37,28 @@ func BuildOfficeUser(db *pop.Connection, customs []Customization, traits []Trait // Find/create the user model user := BuildUserAndUsersRoles(db, customs, nil) - var closeoutOffice models.TransportationOffice + var transportationOffice models.TransportationOffice tempCloseoutOfficeCustoms := customs closeoutOfficeResult := findValidCustomization(customs, TransportationOffices.CloseoutOffice) if closeoutOfficeResult != nil { tempCloseoutOfficeCustoms = convertCustomizationInList(tempCloseoutOfficeCustoms, TransportationOffices.CloseoutOffice, TransportationOffice) - closeoutOffice = BuildTransportationOffice(db, tempCloseoutOfficeCustoms, nil) + transportationOffice = BuildTransportationOffice(db, tempCloseoutOfficeCustoms, nil) + } else { + transportationOffice = BuildTransportationOffice(db, customs, nil) } - // Find/create the TransportationOffice model - basicTransportationOffice := BuildTransportationOffice(db, customs, nil) // create officeuser officeUser := models.OfficeUser{ - UserID: &user.ID, - User: user, - FirstName: "Leo", - LastName: "Spaceman", - Email: "leo_spaceman_office@example.com", - Telephone: "415-555-1212", + UserID: &user.ID, + User: user, + FirstName: "Leo", + LastName: "Spaceman", + Email: "leo_spaceman_office@example.com", + Telephone: "415-555-1212", + TransportationOffice: transportationOffice, + TransportationOfficeID: transportationOffice.ID, } - // DAD TODO: remove model or ID? - if closeoutOfficeResult != nil { - officeUser.TransportationOffice = closeoutOffice - officeUser.TransportationOfficeID = closeoutOffice.ID - } else { - officeUser.TransportationOffice = basicTransportationOffice - officeUser.TransportationOfficeID = basicTransportationOffice.ID - } // Overwrite values with those from assertions testdatagen.MergeModels(&officeUser, cOfficeUser) From 6ce62c84a73fc41d7eda7e9878081b7fab673382 Mon Sep 17 00:00:00 2001 From: Paul Stonebraker Date: Tue, 17 Dec 2024 15:21:39 +0000 Subject: [PATCH 08/18] generate mocks --- pkg/services/mocks/OfficeUserFetcherPop.go | 30 ++++++++++++++++++++++ 1 file changed, 30 insertions(+) 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) From 3ef9a60b9fe1b3f11ee29c26f55b028bd023cfb9 Mon Sep 17 00:00:00 2001 From: Paul Stonebraker Date: Tue, 17 Dec 2024 16:54:20 +0000 Subject: [PATCH 09/18] add code back in --- pkg/factory/move_factory.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/factory/move_factory.go b/pkg/factory/move_factory.go index 26f52cd17f6..3ac02f48e0a 100644 --- a/pkg/factory/move_factory.go +++ b/pkg/factory/move_factory.go @@ -77,11 +77,14 @@ func BuildMove(db *pop.Connection, customs []Customization, traits []Trait) mode } if closeoutOfficeResult != nil { + move.CloseoutOffice = &closeoutOffice move.CloseoutOfficeID = &closeoutOffice.ID + move.CounselingOffice = &closeoutOffice move.CounselingOfficeID = &closeoutOffice.ID } if scAssignedUserResult != nil { + move.SCAssignedUser = &scAssignedUser move.SCAssignedID = &scAssignedUser.ID } From 9a45b1256e25ade0f5465544557a472cdd66cb59 Mon Sep 17 00:00:00 2001 From: Paul Stonebraker Date: Tue, 17 Dec 2024 18:49:31 +0000 Subject: [PATCH 10/18] fix test timeout; add check for active office user --- .../office_user/office_user_fetcher.go | 1 + .../office_user/office_user_fetcher_test.go | 49 ++++++++++--------- 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/pkg/services/office_user/office_user_fetcher.go b/pkg/services/office_user/office_user_fetcher.go index dc1ff6df1ef..066ff342111 100644 --- a/pkg/services/office_user/office_user_fetcher.go +++ b/pkg/services/office_user/office_user_fetcher.go @@ -152,6 +152,7 @@ func (o *officeUserFetcherPop) FetchOfficeUsersWithWorkloadByRoleAndOffice(appCt ) 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) diff --git a/pkg/services/office_user/office_user_fetcher_test.go b/pkg/services/office_user/office_user_fetcher_test.go index f409359a7be..4c1b6367ee4 100644 --- a/pkg/services/office_user/office_user_fetcher_test.go +++ b/pkg/services/office_user/office_user_fetcher_test.go @@ -104,30 +104,9 @@ func (suite *OfficeUserServiceSuite) TestFetchOfficeUserByID() { }) } -func (suite *OfficeUserServiceSuite) TestFetchOfficeUsersByRoleAndOffice() { - 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 - factory.BuildOfficeUserWithRoles(suite.DB(), factory.GetTraitActiveOfficeUser(), []roles.RoleType{roles.RoleTypeServicesCounselor}) - factory.BuildOfficeUserWithRoles(suite.DB(), factory.GetTraitActiveOfficeUser(), []roles.RoleType{roles.RoleTypeServicesCounselor}) - 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) - - // ensure length of returned set is 1, corresponding to the TOO role passed to FetchOfficeUsersByRoleAndOffice - // and not 2 (SC) or 3 (TIO) - suite.NoError(err) - suite.Len(fetchedUsers, 1) - }) -} - func (suite *OfficeUserServiceSuite) TestFetchOfficeUsersWithWorkloadByRoleAndOffice() { - suite.Run("FetchOfficeUsersWithWorkloadByRoleAndOffice returns an office user's name, id, and workload when given a role and office", func() { - fetcher := NewOfficeUserFetcherPop() + 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(), []factory.Customization{ { Model: models.TransportationOffice{ @@ -142,6 +121,11 @@ func (suite *OfficeUserServiceSuite) TestFetchOfficeUsersWithWorkloadByRoleAndOf LinkOnly: true, Type: &factory.TransportationOffices.CloseoutOffice, }, + { + Model: models.OfficeUser{ + Active: true, + }, + }, }, []roles.RoleType{roles.RoleTypeServicesCounselor}) factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ @@ -168,4 +152,23 @@ func (suite *OfficeUserServiceSuite) TestFetchOfficeUsersWithWorkloadByRoleAndOf 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 + factory.BuildOfficeUserWithRoles(suite.DB(), factory.GetTraitActiveOfficeUser(), []roles.RoleType{roles.RoleTypeServicesCounselor}) + factory.BuildOfficeUserWithRoles(suite.DB(), factory.GetTraitActiveOfficeUser(), []roles.RoleType{roles.RoleTypeServicesCounselor}) + 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) + + // ensure length of returned set is 1, corresponding to the TOO role passed to FetchOfficeUsersByRoleAndOffice + // and not 2 (SC) or 3 (TIO) + suite.NoError(err) + suite.Len(fetchedUsers, 1) + }) } From ce2c16ba66425965d2fceabe0089b5bb76111869 Mon Sep 17 00:00:00 2001 From: Paul Stonebraker Date: Tue, 17 Dec 2024 19:31:22 +0000 Subject: [PATCH 11/18] update to accept counseling office --- pkg/factory/move_factory.go | 15 +++- pkg/factory/office_user_factory.go | 5 ++ pkg/services/move/move_fetcher_test.go | 72 ++++++------------- .../office_user/office_user_fetcher_test.go | 13 +--- 4 files changed, 42 insertions(+), 63 deletions(-) diff --git a/pkg/factory/move_factory.go b/pkg/factory/move_factory.go index 3ac02f48e0a..15b0fb4154c 100644 --- a/pkg/factory/move_factory.go +++ b/pkg/factory/move_factory.go @@ -35,6 +35,14 @@ 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) @@ -79,8 +87,11 @@ func BuildMove(db *pop.Connection, customs []Customization, traits []Trait) mode if closeoutOfficeResult != nil { move.CloseoutOffice = &closeoutOffice move.CloseoutOfficeID = &closeoutOffice.ID - move.CounselingOffice = &closeoutOffice - move.CounselingOfficeID = &closeoutOffice.ID + } + + if counselingOfficeResult != nil { + move.CounselingOffice = &counselingOffice + move.CounselingOfficeID = &counselingOffice.ID } if scAssignedUserResult != nil { diff --git a/pkg/factory/office_user_factory.go b/pkg/factory/office_user_factory.go index ac0c82284f0..af96f80eae8 100644 --- a/pkg/factory/office_user_factory.go +++ b/pkg/factory/office_user_factory.go @@ -39,10 +39,15 @@ func BuildOfficeUser(db *pop.Connection, customs []Customization, traits []Trait var transportationOffice models.TransportationOffice tempCloseoutOfficeCustoms := customs + tempCounselingOfficeCustoms := customs closeoutOfficeResult := findValidCustomization(customs, TransportationOffices.CloseoutOffice) + counselingOfficeResult := findValidCustomization(customs, TransportationOffices.CounselingOffice) if closeoutOfficeResult != nil { tempCloseoutOfficeCustoms = convertCustomizationInList(tempCloseoutOfficeCustoms, TransportationOffices.CloseoutOffice, TransportationOffice) transportationOffice = BuildTransportationOffice(db, tempCloseoutOfficeCustoms, nil) + } else if counselingOfficeResult != nil { + tempCounselingOfficeCustoms = convertCustomizationInList(tempCounselingOfficeCustoms, TransportationOffices.CounselingOffice, TransportationOffice) + transportationOffice = BuildTransportationOffice(db, tempCounselingOfficeCustoms, nil) } else { transportationOffice = BuildTransportationOffice(db, customs, nil) } diff --git a/pkg/services/move/move_fetcher_test.go b/pkg/services/move/move_fetcher_test.go index 7a0dab1d277..ebb85bcc67f 100644 --- a/pkg/services/move/move_fetcher_test.go +++ b/pkg/services/move/move_fetcher_test.go @@ -108,13 +108,7 @@ func (suite *MoveServiceSuite) TestMoveFetcher() { func (suite *MoveServiceSuite) TestMoveFetcherBulkAssignment() { setupTestData := func() (services.MoveFetcherBulkAssignment, models.Move, models.TransportationOffice, models.OfficeUser) { moveFetcher := NewMoveFetcherBulkAssignment() - transportationOffice := factory.BuildTransportationOffice(suite.DB(), []factory.Customization{ - { - Model: models.TransportationOffice{ - ProvidesCloseout: true, - }, - }, - }, nil) + 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 @@ -127,7 +121,7 @@ func (suite *MoveServiceSuite) TestMoveFetcherBulkAssignment() { { Model: transportationOffice, LinkOnly: true, - Type: &factory.TransportationOffices.CloseoutOffice, + Type: &factory.TransportationOffices.CounselingOffice, }, }, nil) @@ -140,7 +134,7 @@ func (suite *MoveServiceSuite) TestMoveFetcherBulkAssignment() { { Model: transportationOffice, LinkOnly: true, - Type: &factory.TransportationOffices.CloseoutOffice, + Type: &factory.TransportationOffices.CounselingOffice, }, }, nil) @@ -148,7 +142,7 @@ func (suite *MoveServiceSuite) TestMoveFetcherBulkAssignment() { { Model: transportationOffice, LinkOnly: true, - Type: &factory.TransportationOffices.CloseoutOffice, + Type: &factory.TransportationOffices.CounselingOffice, }, }, []roles.RoleType{roles.RoleTypeServicesCounselor}) @@ -164,13 +158,7 @@ func (suite *MoveServiceSuite) TestMoveFetcherBulkAssignment() { suite.Run("Does not return moves that are counseled by a different counseling office", func() { moveFetcher, _, _, officeUser := setupTestData() - transportationOffice := factory.BuildTransportationOffice(suite.DB(), []factory.Customization{ - { - Model: models.TransportationOffice{ - ProvidesCloseout: true, - }, - }, - }, nil) + transportationOffice := factory.BuildTransportationOffice(suite.DB(), nil, nil) factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ { @@ -181,13 +169,13 @@ func (suite *MoveServiceSuite) TestMoveFetcherBulkAssignment() { { Model: transportationOffice, LinkOnly: true, - Type: &factory.TransportationOffices.CloseoutOffice, + Type: &factory.TransportationOffices.CounselingOffice, }, }, nil) moves, err := moveFetcher.FetchMovesForBulkAssignmentCounseling(suite.AppContextForTest(), "KKFA", officeUser.TransportationOffice.ID) suite.FatalNoError(err) - suite.Equal(len(moves), 2) + suite.Equal(2, len(moves)) }) suite.Run("Does not return moves with safety, bluebark, or wounded warrior order types", func() { @@ -206,7 +194,7 @@ func (suite *MoveServiceSuite) TestMoveFetcherBulkAssignment() { { Model: transportationOffice, LinkOnly: true, - Type: &factory.TransportationOffices.CloseoutOffice, + Type: &factory.TransportationOffices.CounselingOffice, }, }, nil) factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ @@ -223,7 +211,7 @@ func (suite *MoveServiceSuite) TestMoveFetcherBulkAssignment() { { Model: transportationOffice, LinkOnly: true, - Type: &factory.TransportationOffices.CloseoutOffice, + Type: &factory.TransportationOffices.CounselingOffice, }, }, nil) factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ @@ -240,7 +228,7 @@ func (suite *MoveServiceSuite) TestMoveFetcherBulkAssignment() { { Model: transportationOffice, LinkOnly: true, - Type: &factory.TransportationOffices.CloseoutOffice, + Type: &factory.TransportationOffices.CounselingOffice, }, }, nil) @@ -253,18 +241,12 @@ func (suite *MoveServiceSuite) TestMoveFetcherBulkAssignment() { // 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(), []factory.Customization{ - { - Model: models.TransportationOffice{ - ProvidesCloseout: true, - }, - }, - }, nil) + transportationOffice := factory.BuildTransportationOffice(suite.DB(), nil, nil) moveWithPPM := factory.BuildMoveWithPPMShipment(suite.DB(), []factory.Customization{ { Model: transportationOffice, LinkOnly: true, - Type: &factory.TransportationOffices.CloseoutOffice, + Type: &factory.TransportationOffices.CounselingOffice, }, { Model: models.PPMShipment{ @@ -277,7 +259,7 @@ func (suite *MoveServiceSuite) TestMoveFetcherBulkAssignment() { { Model: transportationOffice, LinkOnly: true, - Type: &factory.TransportationOffices.CloseoutOffice, + Type: &factory.TransportationOffices.CounselingOffice, }, }, []roles.RoleType{roles.RoleTypeServicesCounselor}) @@ -304,18 +286,12 @@ func (suite *MoveServiceSuite) TestMoveFetcherBulkAssignment() { suite.Run("Does not return moves with PPMs in wneeds closeout", func() { moveFetcher := NewMoveFetcherBulkAssignment() - transportationOffice := factory.BuildTransportationOffice(suite.DB(), []factory.Customization{ - { - Model: models.TransportationOffice{ - ProvidesCloseout: true, - }, - }, - }, nil) + transportationOffice := factory.BuildTransportationOffice(suite.DB(), nil, nil) moveWithPPM := factory.BuildMoveWithPPMShipment(suite.DB(), []factory.Customization{ { Model: transportationOffice, LinkOnly: true, - Type: &factory.TransportationOffices.CloseoutOffice, + Type: &factory.TransportationOffices.CounselingOffice, }, { Model: models.PPMShipment{ @@ -328,7 +304,7 @@ func (suite *MoveServiceSuite) TestMoveFetcherBulkAssignment() { { Model: transportationOffice, LinkOnly: true, - Type: &factory.TransportationOffices.CloseoutOffice, + Type: &factory.TransportationOffices.CounselingOffice, }, }, []roles.RoleType{roles.RoleTypeServicesCounselor}) @@ -365,7 +341,7 @@ func (suite *MoveServiceSuite) TestMoveFetcherBulkAssignment() { { Model: transportationOffice, LinkOnly: true, - Type: &factory.TransportationOffices.CloseoutOffice, + Type: &factory.TransportationOffices.CounselingOffice, }, { Model: models.PPMShipment{ @@ -378,7 +354,7 @@ func (suite *MoveServiceSuite) TestMoveFetcherBulkAssignment() { { Model: transportationOffice, LinkOnly: true, - Type: &factory.TransportationOffices.CloseoutOffice, + Type: &factory.TransportationOffices.CounselingOffice, }, }, []roles.RoleType{roles.RoleTypeServicesCounselor}) @@ -406,19 +382,13 @@ func (suite *MoveServiceSuite) TestMoveFetcherBulkAssignment() { suite.Run("Does not return moves that are already assigned", func() { // moveFetcher, _, transOffice, officeUser := setupTestData() moveFetcher := NewMoveFetcherBulkAssignment() - transportationOffice := factory.BuildTransportationOffice(suite.DB(), []factory.Customization{ - { - Model: models.TransportationOffice{ - ProvidesCloseout: true, - }, - }, - }, nil) + transportationOffice := factory.BuildTransportationOffice(suite.DB(), nil, nil) officeUser := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ { Model: transportationOffice, LinkOnly: true, - Type: &factory.TransportationOffices.CloseoutOffice, + Type: &factory.TransportationOffices.CounselingOffice, }, }, []roles.RoleType{roles.RoleTypeServicesCounselor}) @@ -431,7 +401,7 @@ func (suite *MoveServiceSuite) TestMoveFetcherBulkAssignment() { { Model: transportationOffice, LinkOnly: true, - Type: &factory.TransportationOffices.CloseoutOffice, + Type: &factory.TransportationOffices.CounselingOffice, }, { Model: officeUser, diff --git a/pkg/services/office_user/office_user_fetcher_test.go b/pkg/services/office_user/office_user_fetcher_test.go index 4c1b6367ee4..1a2f1763c95 100644 --- a/pkg/services/office_user/office_user_fetcher_test.go +++ b/pkg/services/office_user/office_user_fetcher_test.go @@ -107,19 +107,13 @@ func (suite *OfficeUserServiceSuite) TestFetchOfficeUserByID() { 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(), []factory.Customization{ - { - Model: models.TransportationOffice{ - ProvidesCloseout: true, - }, - }, - }, nil) + transportationOffice := factory.BuildTransportationOffice(suite.DB(), nil, nil) officeUser := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ { Model: transportationOffice, LinkOnly: true, - Type: &factory.TransportationOffices.CloseoutOffice, + Type: &factory.TransportationOffices.CounselingOffice, }, { Model: models.OfficeUser{ @@ -137,7 +131,7 @@ func (suite *OfficeUserServiceSuite) TestFetchOfficeUsersWithWorkloadByRoleAndOf { Model: transportationOffice, LinkOnly: true, - Type: &factory.TransportationOffices.CloseoutOffice, + Type: &factory.TransportationOffices.CounselingOffice, }, { Model: officeUser, @@ -162,7 +156,6 @@ func (suite *OfficeUserServiceSuite) TestFetchOfficeUsersWithWorkloadByRoleAndOf 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) From b3de9c1f63bb54839a00d356854edd5819ac4ac7 Mon Sep 17 00:00:00 2001 From: Paul Stonebraker Date: Tue, 17 Dec 2024 19:35:46 +0000 Subject: [PATCH 12/18] fix conditional --- pkg/factory/office_user_factory.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/pkg/factory/office_user_factory.go b/pkg/factory/office_user_factory.go index af96f80eae8..4f92bbf8a53 100644 --- a/pkg/factory/office_user_factory.go +++ b/pkg/factory/office_user_factory.go @@ -38,17 +38,22 @@ func BuildOfficeUser(db *pop.Connection, customs []Customization, traits []Trait user := BuildUserAndUsersRoles(db, customs, nil) var transportationOffice models.TransportationOffice + tempCloseoutOfficeCustoms := customs - tempCounselingOfficeCustoms := customs closeoutOfficeResult := findValidCustomization(customs, TransportationOffices.CloseoutOffice) - counselingOfficeResult := findValidCustomization(customs, TransportationOffices.CounselingOffice) if closeoutOfficeResult != nil { tempCloseoutOfficeCustoms = convertCustomizationInList(tempCloseoutOfficeCustoms, TransportationOffices.CloseoutOffice, TransportationOffice) transportationOffice = BuildTransportationOffice(db, tempCloseoutOfficeCustoms, nil) - } else if counselingOfficeResult != nil { + } + + tempCounselingOfficeCustoms := customs + counselingOfficeResult := findValidCustomization(customs, TransportationOffices.CounselingOffice) + if counselingOfficeResult != nil { tempCounselingOfficeCustoms = convertCustomizationInList(tempCounselingOfficeCustoms, TransportationOffices.CounselingOffice, TransportationOffice) transportationOffice = BuildTransportationOffice(db, tempCounselingOfficeCustoms, nil) - } else { + } + + if closeoutOfficeResult == nil && counselingOfficeResult == nil { transportationOffice = BuildTransportationOffice(db, customs, nil) } From 51d50bb4c0c39a24a11ab7f33e3cf3a6df1099fe Mon Sep 17 00:00:00 2001 From: Paul Stonebraker Date: Tue, 17 Dec 2024 19:47:40 +0000 Subject: [PATCH 13/18] update office user factory --- pkg/factory/office_user_factory.go | 37 +++++++++++++++++------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/pkg/factory/office_user_factory.go b/pkg/factory/office_user_factory.go index 4f92bbf8a53..09b8bb0b154 100644 --- a/pkg/factory/office_user_factory.go +++ b/pkg/factory/office_user_factory.go @@ -218,33 +218,38 @@ func BuildOfficeUserWithPrivileges(db *pop.Connection, customs []Customization, user := BuildUserAndUsersRolesAndUsersPrivileges(db, customs, nil) // Find/create the TransportationOffice model - basicTransportationOffice := BuildTransportationOffice(db, customs, nil) + var transportationOffice models.TransportationOffice - var closeoutOffice models.TransportationOffice tempCloseoutOfficeCustoms := customs closeoutOfficeResult := findValidCustomization(customs, TransportationOffices.CloseoutOffice) if closeoutOfficeResult != nil { tempCloseoutOfficeCustoms = convertCustomizationInList(tempCloseoutOfficeCustoms, TransportationOffices.CloseoutOffice, TransportationOffice) - closeoutOffice = BuildTransportationOffice(db, tempCloseoutOfficeCustoms, nil) + 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{ - UserID: &user.ID, - User: user, - FirstName: "Leo", - LastName: "Spaceman", - Email: "leo_spaceman_office@example.com", - Telephone: "415-555-1212", + UserID: &user.ID, + User: user, + FirstName: "Leo", + LastName: "Spaceman", + Email: "leo_spaceman_office@example.com", + Telephone: "415-555-1212", + TransportationOffice: transportationOffice, + TransportationOfficeID: transportationOffice.ID, } - if closeoutOfficeResult != nil { - officeUser.TransportationOffice = closeoutOffice - officeUser.TransportationOfficeID = closeoutOffice.ID - } else { - officeUser.TransportationOffice = basicTransportationOffice - officeUser.TransportationOfficeID = basicTransportationOffice.ID - } // Overwrite values with those from assertions testdatagen.MergeModels(&officeUser, cOfficeUser) From adf967d1ff40ed54f60a8b72eb5d6980cd333310 Mon Sep 17 00:00:00 2001 From: Paul Stonebraker Date: Tue, 17 Dec 2024 19:54:07 +0000 Subject: [PATCH 14/18] add test I had removed for some reason? --- pkg/services/office_user/office_user_fetcher_test.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pkg/services/office_user/office_user_fetcher_test.go b/pkg/services/office_user/office_user_fetcher_test.go index 1a2f1763c95..f5a82564a69 100644 --- a/pkg/services/office_user/office_user_fetcher_test.go +++ b/pkg/services/office_user/office_user_fetcher_test.go @@ -164,4 +164,12 @@ func (suite *OfficeUserServiceSuite) TestFetchOfficeUsersWithWorkloadByRoleAndOf suite.NoError(err) suite.Len(fetchedUsers, 1) }) + + suite.Run("returns zero value office user on error", func() { + officeUser, err := fetcher.FetchOfficeUserByID(suite.AppContextForTest(), uuid.Nil) + + suite.Error(err) + suite.IsType(apperror.NotFoundError{}, err) + suite.Equal(uuid.Nil, officeUser.ID) + }) } From 778fc20e319acab8491bb72266f853ac278e2bbb Mon Sep 17 00:00:00 2001 From: Paul Stonebraker Date: Tue, 17 Dec 2024 20:26:36 +0000 Subject: [PATCH 15/18] update test and remove print statement --- pkg/handlers/ghcapi/queues.go | 3 +-- pkg/handlers/ghcapi/queues_test.go | 15 +++++---------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/pkg/handlers/ghcapi/queues.go b/pkg/handlers/ghcapi/queues.go index 95e26eed6b9..16c80507c85 100644 --- a/pkg/handlers/ghcapi/queues.go +++ b/pkg/handlers/ghcapi/queues.go @@ -610,8 +610,7 @@ func (h GetBulkAssignmentDataHandler) Handle( appCtx.Logger().Error("Error retreiving moves", zap.Error(err)) return queues.NewGetBulkAssignmentDataInternalServerError(), err } - fmt.Println("moves") - fmt.Println(moves) + officeUserData = payloads.BulkAssignmentData(appCtx, moves, officeUsers, officeUser.TransportationOffice.ID) } return queues.NewGetBulkAssignmentDataOK().WithPayload(&officeUserData), nil diff --git a/pkg/handlers/ghcapi/queues_test.go b/pkg/handlers/ghcapi/queues_test.go index 904744adc9e..8d5c475929f 100644 --- a/pkg/handlers/ghcapi/queues_test.go +++ b/pkg/handlers/ghcapi/queues_test.go @@ -1678,24 +1678,19 @@ func (suite *HandlerSuite) TestGetBulkAssignmentDataHandler() { suite.IsType(&queues.GetBulkAssignmentDataUnauthorized{}, response) }) suite.Run("returns properly formatted bulk assignment data", func() { - transportationOffice := factory.BuildTransportationOffice(suite.DB(), []factory.Customization{ - { - Model: models.TransportationOffice{ - ProvidesCloseout: true, - }, - }, - }, nil) + transportationOffice := factory.BuildTransportationOffice(suite.DB(), nil, nil) officeUser := factory.BuildOfficeUserWithPrivileges(suite.DB(), []factory.Customization{ { Model: models.OfficeUser{ - Email: "officeuser1@example.com", + Email: "officeuser1@example.com", + Active: true, }, }, { Model: transportationOffice, LinkOnly: true, - Type: &factory.TransportationOffices.CloseoutOffice, + Type: &factory.TransportationOffices.CounselingOffice, }, { Model: models.User{ @@ -1723,7 +1718,7 @@ func (suite *HandlerSuite) TestGetBulkAssignmentDataHandler() { { Model: transportationOffice, LinkOnly: true, - Type: &factory.TransportationOffices.CloseoutOffice, + Type: &factory.TransportationOffices.CounselingOffice, }, }, nil) From c8f8aa150bf0a60d6ee2246703253f85553a0c67 Mon Sep 17 00:00:00 2001 From: Paul Stonebraker Date: Thu, 19 Dec 2024 16:50:05 +0000 Subject: [PATCH 16/18] update test to make more sense --- pkg/services/move/move_fetcher_test.go | 55 ++++++++++++++------------ 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/pkg/services/move/move_fetcher_test.go b/pkg/services/move/move_fetcher_test.go index ebb85bcc67f..381199013cd 100644 --- a/pkg/services/move/move_fetcher_test.go +++ b/pkg/services/move/move_fetcher_test.go @@ -234,7 +234,7 @@ func (suite *MoveServiceSuite) TestMoveFetcherBulkAssignment() { moves, err := moveFetcher.FetchMovesForBulkAssignmentCounseling(suite.AppContextForTest(), "KKFA", officeUser.TransportationOffice.ID) suite.FatalNoError(err) - suite.Equal(len(moves), 2) + suite.Equal(2, len(moves)) }) // BuildMoveWithPPMShipment apparently builds 3 moves each time its run, so the best way @@ -242,7 +242,7 @@ func (suite *MoveServiceSuite) TestMoveFetcherBulkAssignment() { suite.Run("Does not return moves with PPMs in waiting on customer status", func() { moveFetcher := NewMoveFetcherBulkAssignment() transportationOffice := factory.BuildTransportationOffice(suite.DB(), nil, nil) - moveWithPPM := factory.BuildMoveWithPPMShipment(suite.DB(), []factory.Customization{ + moveWithWaitingOnCustomerPPM := factory.BuildMoveWithPPMShipment(suite.DB(), []factory.Customization{ { Model: transportationOffice, LinkOnly: true, @@ -268,26 +268,27 @@ func (suite *MoveServiceSuite) TestMoveFetcherBulkAssignment() { // 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, moveWithPPM.ID) + 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(moveWithPPM.Status, models.MoveStatusNeedsServiceCounseling) + suite.Equal(moveWithWaitingOnCustomerPPM.Status, models.MoveStatusNeedsServiceCounseling) // move is not assigned to anyone - suite.Nil(moveWithPPM.SCAssignedID) + suite.Nil(moveWithWaitingOnCustomerPPM.SCAssignedID) // GBLOC is the same - suite.Equal(*moveWithPPM.Orders.OriginDutyLocationGBLOC, officeUser.TransportationOffice.Gbloc) + suite.Equal(*moveWithWaitingOnCustomerPPM.Orders.OriginDutyLocationGBLOC, officeUser.TransportationOffice.Gbloc) // Show is true - suite.Equal(moveWithPPM.Show, models.BoolPointer(true)) + suite.Equal(moveWithWaitingOnCustomerPPM.Show, models.BoolPointer(true)) // Move is counseled by the office user's office - suite.Equal(*moveWithPPM.CounselingOfficeID, officeUser.TransportationOfficeID) + suite.Equal(*moveWithWaitingOnCustomerPPM.CounselingOfficeID, officeUser.TransportationOfficeID) // Orders type isn't WW, BB, or Safety - suite.Equal(moveWithPPM.Orders.OrdersType, internalmessages.OrdersTypePERMANENTCHANGEOFSTATION) + suite.Equal(moveWithWaitingOnCustomerPPM.Orders.OrdersType, internalmessages.OrdersTypePERMANENTCHANGEOFSTATION) }) - suite.Run("Does not return moves with PPMs in wneeds closeout", func() { + suite.Run("Does not return moves with PPMs in needs closeout status", func() { moveFetcher := NewMoveFetcherBulkAssignment() transportationOffice := factory.BuildTransportationOffice(suite.DB(), nil, nil) - moveWithPPM := factory.BuildMoveWithPPMShipment(suite.DB(), []factory.Customization{ + moveWithNeedsCloseoutPPM := factory.BuildMoveWithPPMShipment(suite.DB(), []factory.Customization{ { Model: transportationOffice, LinkOnly: true, @@ -313,20 +314,21 @@ func (suite *MoveServiceSuite) TestMoveFetcherBulkAssignment() { // 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, moveWithPPM.ID) + 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(moveWithPPM.Status, models.MoveStatusNeedsServiceCounseling) + suite.Equal(moveWithNeedsCloseoutPPM.Status, models.MoveStatusNeedsServiceCounseling) // move is not assigned to anyone - suite.Nil(moveWithPPM.SCAssignedID) + suite.Nil(moveWithNeedsCloseoutPPM.SCAssignedID) // GBLOC is the same - suite.Equal(*moveWithPPM.Orders.OriginDutyLocationGBLOC, officeUser.TransportationOffice.Gbloc) + suite.Equal(*moveWithNeedsCloseoutPPM.Orders.OriginDutyLocationGBLOC, officeUser.TransportationOffice.Gbloc) // Show is true - suite.Equal(moveWithPPM.Show, models.BoolPointer(true)) + suite.Equal(moveWithNeedsCloseoutPPM.Show, models.BoolPointer(true)) // Move is counseled by the office user's office - suite.Equal(*moveWithPPM.CounselingOfficeID, officeUser.TransportationOfficeID) + suite.Equal(*moveWithNeedsCloseoutPPM.CounselingOfficeID, officeUser.TransportationOfficeID) // Orders type isn't WW, BB, or Safety - suite.Equal(moveWithPPM.Orders.OrdersType, internalmessages.OrdersTypePERMANENTCHANGEOFSTATION) + suite.Equal(moveWithNeedsCloseoutPPM.Orders.OrdersType, internalmessages.OrdersTypePERMANENTCHANGEOFSTATION) }) suite.Run("Does not return moves with PPMs in closeout complete status", func() { moveFetcher := NewMoveFetcherBulkAssignment() @@ -337,7 +339,7 @@ func (suite *MoveServiceSuite) TestMoveFetcherBulkAssignment() { }, }, }, nil) - moveWithPPM := factory.BuildMoveWithPPMShipment(suite.DB(), []factory.Customization{ + moveWithCloseoutCompletePPM := factory.BuildMoveWithPPMShipment(suite.DB(), []factory.Customization{ { Model: transportationOffice, LinkOnly: true, @@ -363,20 +365,21 @@ func (suite *MoveServiceSuite) TestMoveFetcherBulkAssignment() { // 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, moveWithPPM.ID) + 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(moveWithPPM.Status, models.MoveStatusNeedsServiceCounseling) + suite.Equal(moveWithCloseoutCompletePPM.Status, models.MoveStatusNeedsServiceCounseling) // move is not assigned to anyone - suite.Nil(moveWithPPM.SCAssignedID) + suite.Nil(moveWithCloseoutCompletePPM.SCAssignedID) // GBLOC is the same - suite.Equal(*moveWithPPM.Orders.OriginDutyLocationGBLOC, officeUser.TransportationOffice.Gbloc) + suite.Equal(*moveWithCloseoutCompletePPM.Orders.OriginDutyLocationGBLOC, officeUser.TransportationOffice.Gbloc) // Show is true - suite.Equal(moveWithPPM.Show, models.BoolPointer(true)) + suite.Equal(moveWithCloseoutCompletePPM.Show, models.BoolPointer(true)) // Move is counseled by the office user's office - suite.Equal(*moveWithPPM.CounselingOfficeID, officeUser.TransportationOfficeID) + suite.Equal(*moveWithCloseoutCompletePPM.CounselingOfficeID, officeUser.TransportationOfficeID) // Orders type isn't WW, BB, or Safety - suite.Equal(moveWithPPM.Orders.OrdersType, internalmessages.OrdersTypePERMANENTCHANGEOFSTATION) + suite.Equal(moveWithCloseoutCompletePPM.Orders.OrdersType, internalmessages.OrdersTypePERMANENTCHANGEOFSTATION) }) suite.Run("Does not return moves that are already assigned", func() { From 8d308f77ed2d40c85006c4dd1eaa5a7baa6520f9 Mon Sep 17 00:00:00 2001 From: Paul Stonebraker Date: Tue, 7 Jan 2025 14:26:53 +0000 Subject: [PATCH 17/18] update sql to use distinct for workload --- pkg/services/office_user/office_user_fetcher.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/office_user/office_user_fetcher.go b/pkg/services/office_user/office_user_fetcher.go index 066ff342111..80d178221f4 100644 --- a/pkg/services/office_user/office_user_fetcher.go +++ b/pkg/services/office_user/office_user_fetcher.go @@ -138,7 +138,7 @@ func (o *officeUserFetcherPop) FetchOfficeUsersWithWorkloadByRoleAndOffice(appCt query := `SELECT ou.id, ou.first_name, ou.last_name, - COUNT(m.id) AS workload + COUNT(DISTINCT 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 From 7923674c344e1693154b11885ae3d293497dd970 Mon Sep 17 00:00:00 2001 From: Paul Stonebraker Date: Tue, 7 Jan 2025 16:05:20 +0000 Subject: [PATCH 18/18] remove extra join --- .../office_user/office_user_fetcher.go | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/pkg/services/office_user/office_user_fetcher.go b/pkg/services/office_user/office_user_fetcher.go index 80d178221f4..350162324d2 100644 --- a/pkg/services/office_user/office_user_fetcher.go +++ b/pkg/services/office_user/office_user_fetcher.go @@ -135,27 +135,27 @@ func (o *officeUserFetcherPop) FetchSafetyMoveOfficeUsersByRoleAndOffice(appCtx 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(DISTINCT 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) + query := + `SELECT office_users.id, + office_users.first_name, + office_users.last_name, + COUNT(DISTINCT moves.id) AS workload + FROM office_users + JOIN users_roles ON office_users.user_id = users_roles.user_id + JOIN roles ON users_roles.role_id = roles.id + JOIN transportation_offices ON office_users.transportation_office_id = transportation_offices.id + LEFT JOIN moves + ON ( + (roles.role_type = 'services_counselor' AND moves.sc_assigned_id = office_users.id) OR + (roles.role_type = 'task_ordering_officer' AND moves.too_assigned_id = office_users.id) OR + (roles.role_type = 'task_invoicing_officer' and moves.tio_assigned_id = office_users.id) + ) + WHERE roles.role_type = $1 + AND transportation_offices.id = $2 + AND office_users.active = TRUE + GROUP BY office_users.id, office_users.first_name, office_users.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) }