diff --git a/pkg/gen/ghcapi/configure_mymove.go b/pkg/gen/ghcapi/configure_mymove.go index 432343d4dab..5719621edd6 100644 --- a/pkg/gen/ghcapi/configure_mymove.go +++ b/pkg/gen/ghcapi/configure_mymove.go @@ -466,6 +466,11 @@ func configureAPI(api *ghcoperations.MymoveAPI) http.Handler { return middleware.NotImplemented("operation shipment.ReviewShipmentAddressUpdate has not yet been implemented") }) } + if api.QueuesSaveBulkAssignmentDataHandler == nil { + api.QueuesSaveBulkAssignmentDataHandler = queues.SaveBulkAssignmentDataHandlerFunc(func(params queues.SaveBulkAssignmentDataParams) middleware.Responder { + return middleware.NotImplemented("operation queues.SaveBulkAssignmentData has not yet been implemented") + }) + } if api.EvaluationReportsSaveEvaluationReportHandler == nil { api.EvaluationReportsSaveEvaluationReportHandler = evaluation_reports.SaveEvaluationReportHandlerFunc(func(params evaluation_reports.SaveEvaluationReportParams) middleware.Responder { return middleware.NotImplemented("operation evaluation_reports.SaveEvaluationReport has not yet been implemented") diff --git a/pkg/gen/ghcapi/embedded_spec.go b/pkg/gen/ghcapi/embedded_spec.go index a1e4e130a28..f6b0b1c08ce 100644 --- a/pkg/gen/ghcapi/embedded_spec.go +++ b/pkg/gen/ghcapi/embedded_spec.go @@ -4416,6 +4416,46 @@ func init() { } } }, + "/queues/bulk-assignment/assign": { + "post": { + "description": "Supervisor office users are able to assign moves. This endpoint saves office user assignments to multiple moves.\n", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "queues" + ], + "summary": "Assigns one or more moves to one or more office users", + "operationId": "saveBulkAssignmentData", + "parameters": [ + { + "name": "bulkAssignmentSavePayload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/BulkAssignmentSavePayload" + } + } + ], + "responses": { + "204": { + "description": "assigned" + }, + "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", @@ -7300,6 +7340,23 @@ func init() { } } }, + "BulkAssignmentForUser": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "moveAssignments": { + "type": "integer", + "x-omitempty": false + } + } + }, + "BulkAssignmentMoveData": { + "type": "string", + "format": "uuid" + }, "BulkAssignmentMoveID": { "type": "string", "format": "uuid", @@ -7311,6 +7368,33 @@ func init() { "$ref": "#/definitions/BulkAssignmentMoveID" } }, + "BulkAssignmentSavePayload": { + "type": "object", + "properties": { + "moveData": { + "type": "array", + "items": { + "$ref": "#/definitions/BulkAssignmentMoveData" + } + }, + "queueType": { + "description": "A string corresponding to the queue type", + "type": "string", + "enum": [ + "COUNSELING", + "CLOSEOUT", + "TASK_ORDER", + "PAYMENT_REQUEST" + ] + }, + "userData": { + "type": "array", + "items": { + "$ref": "#/definitions/BulkAssignmentForUser" + } + } + } + }, "ClientError": { "type": "object", "required": [ @@ -21536,6 +21620,55 @@ func init() { } } }, + "/queues/bulk-assignment/assign": { + "post": { + "description": "Supervisor office users are able to assign moves. This endpoint saves office user assignments to multiple moves.\n", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "queues" + ], + "summary": "Assigns one or more moves to one or more office users", + "operationId": "saveBulkAssignmentData", + "parameters": [ + { + "name": "bulkAssignmentSavePayload", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/BulkAssignmentSavePayload" + } + } + ], + "responses": { + "204": { + "description": "assigned" + }, + "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", @@ -24850,6 +24983,23 @@ func init() { } } }, + "BulkAssignmentForUser": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "moveAssignments": { + "type": "integer", + "x-omitempty": false + } + } + }, + "BulkAssignmentMoveData": { + "type": "string", + "format": "uuid" + }, "BulkAssignmentMoveID": { "type": "string", "format": "uuid", @@ -24861,6 +25011,33 @@ func init() { "$ref": "#/definitions/BulkAssignmentMoveID" } }, + "BulkAssignmentSavePayload": { + "type": "object", + "properties": { + "moveData": { + "type": "array", + "items": { + "$ref": "#/definitions/BulkAssignmentMoveData" + } + }, + "queueType": { + "description": "A string corresponding to the queue type", + "type": "string", + "enum": [ + "COUNSELING", + "CLOSEOUT", + "TASK_ORDER", + "PAYMENT_REQUEST" + ] + }, + "userData": { + "type": "array", + "items": { + "$ref": "#/definitions/BulkAssignmentForUser" + } + } + } + }, "ClientError": { "type": "object", "required": [ diff --git a/pkg/gen/ghcapi/ghcoperations/mymove_api.go b/pkg/gen/ghcapi/ghcoperations/mymove_api.go index ee86793406f..3a513ab2705 100644 --- a/pkg/gen/ghcapi/ghcoperations/mymove_api.go +++ b/pkg/gen/ghcapi/ghcoperations/mymove_api.go @@ -309,6 +309,9 @@ func NewMymoveAPI(spec *loads.Document) *MymoveAPI { ShipmentReviewShipmentAddressUpdateHandler: shipment.ReviewShipmentAddressUpdateHandlerFunc(func(params shipment.ReviewShipmentAddressUpdateParams) middleware.Responder { return middleware.NotImplemented("operation shipment.ReviewShipmentAddressUpdate has not yet been implemented") }), + QueuesSaveBulkAssignmentDataHandler: queues.SaveBulkAssignmentDataHandlerFunc(func(params queues.SaveBulkAssignmentDataParams) middleware.Responder { + return middleware.NotImplemented("operation queues.SaveBulkAssignmentData has not yet been implemented") + }), EvaluationReportsSaveEvaluationReportHandler: evaluation_reports.SaveEvaluationReportHandlerFunc(func(params evaluation_reports.SaveEvaluationReportParams) middleware.Responder { return middleware.NotImplemented("operation evaluation_reports.SaveEvaluationReport has not yet been implemented") }), @@ -616,6 +619,8 @@ type MymoveAPI struct { ShipmentRequestShipmentReweighHandler shipment.RequestShipmentReweighHandler // ShipmentReviewShipmentAddressUpdateHandler sets the operation handler for the review shipment address update operation ShipmentReviewShipmentAddressUpdateHandler shipment.ReviewShipmentAddressUpdateHandler + // QueuesSaveBulkAssignmentDataHandler sets the operation handler for the save bulk assignment data operation + QueuesSaveBulkAssignmentDataHandler queues.SaveBulkAssignmentDataHandler // EvaluationReportsSaveEvaluationReportHandler sets the operation handler for the save evaluation report operation EvaluationReportsSaveEvaluationReportHandler evaluation_reports.SaveEvaluationReportHandler // CustomerSearchCustomersHandler sets the operation handler for the search customers operation @@ -1004,6 +1009,9 @@ func (o *MymoveAPI) Validate() error { if o.ShipmentReviewShipmentAddressUpdateHandler == nil { unregistered = append(unregistered, "shipment.ReviewShipmentAddressUpdateHandler") } + if o.QueuesSaveBulkAssignmentDataHandler == nil { + unregistered = append(unregistered, "queues.SaveBulkAssignmentDataHandler") + } if o.EvaluationReportsSaveEvaluationReportHandler == nil { unregistered = append(unregistered, "evaluation_reports.SaveEvaluationReportHandler") } @@ -1512,6 +1520,10 @@ func (o *MymoveAPI) initHandlerCache() { o.handlers["PATCH"] = make(map[string]http.Handler) } o.handlers["PATCH"]["/shipments/{shipmentID}/review-shipment-address-update"] = shipment.NewReviewShipmentAddressUpdate(o.context, o.ShipmentReviewShipmentAddressUpdateHandler) + if o.handlers["POST"] == nil { + o.handlers["POST"] = make(map[string]http.Handler) + } + o.handlers["POST"]["/queues/bulk-assignment/assign"] = queues.NewSaveBulkAssignmentData(o.context, o.QueuesSaveBulkAssignmentDataHandler) if o.handlers["PUT"] == nil { o.handlers["PUT"] = make(map[string]http.Handler) } diff --git a/pkg/gen/ghcapi/ghcoperations/queues/save_bulk_assignment_data.go b/pkg/gen/ghcapi/ghcoperations/queues/save_bulk_assignment_data.go new file mode 100644 index 00000000000..32537ec144c --- /dev/null +++ b/pkg/gen/ghcapi/ghcoperations/queues/save_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" +) + +// SaveBulkAssignmentDataHandlerFunc turns a function with the right signature into a save bulk assignment data handler +type SaveBulkAssignmentDataHandlerFunc func(SaveBulkAssignmentDataParams) middleware.Responder + +// Handle executing the request and returning a response +func (fn SaveBulkAssignmentDataHandlerFunc) Handle(params SaveBulkAssignmentDataParams) middleware.Responder { + return fn(params) +} + +// SaveBulkAssignmentDataHandler interface for that can handle valid save bulk assignment data params +type SaveBulkAssignmentDataHandler interface { + Handle(SaveBulkAssignmentDataParams) middleware.Responder +} + +// NewSaveBulkAssignmentData creates a new http.Handler for the save bulk assignment data operation +func NewSaveBulkAssignmentData(ctx *middleware.Context, handler SaveBulkAssignmentDataHandler) *SaveBulkAssignmentData { + return &SaveBulkAssignmentData{Context: ctx, Handler: handler} +} + +/* + SaveBulkAssignmentData swagger:route POST /queues/bulk-assignment/assign queues saveBulkAssignmentData + +# Assigns one or more moves to one or more office users + +Supervisor office users are able to assign moves. This endpoint saves office user assignments to multiple moves. +*/ +type SaveBulkAssignmentData struct { + Context *middleware.Context + Handler SaveBulkAssignmentDataHandler +} + +func (o *SaveBulkAssignmentData) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + route, rCtx, _ := o.Context.RouteInfo(r) + if rCtx != nil { + *r = *rCtx + } + var Params = NewSaveBulkAssignmentDataParams() + 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/save_bulk_assignment_data_parameters.go b/pkg/gen/ghcapi/ghcoperations/queues/save_bulk_assignment_data_parameters.go new file mode 100644 index 00000000000..87d3a9ca0f8 --- /dev/null +++ b/pkg/gen/ghcapi/ghcoperations/queues/save_bulk_assignment_data_parameters.go @@ -0,0 +1,84 @@ +// 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 ( + "io" + "net/http" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + "github.com/go-openapi/runtime/middleware" + "github.com/go-openapi/validate" + + "github.com/transcom/mymove/pkg/gen/ghcmessages" +) + +// NewSaveBulkAssignmentDataParams creates a new SaveBulkAssignmentDataParams object +// +// There are no default values defined in the spec. +func NewSaveBulkAssignmentDataParams() SaveBulkAssignmentDataParams { + + return SaveBulkAssignmentDataParams{} +} + +// SaveBulkAssignmentDataParams contains all the bound params for the save bulk assignment data operation +// typically these are obtained from a http.Request +// +// swagger:parameters saveBulkAssignmentData +type SaveBulkAssignmentDataParams struct { + + // HTTP Request Object + HTTPRequest *http.Request `json:"-"` + + /* + Required: true + In: body + */ + BulkAssignmentSavePayload *ghcmessages.BulkAssignmentSavePayload +} + +// 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 NewSaveBulkAssignmentDataParams() beforehand. +func (o *SaveBulkAssignmentDataParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error { + var res []error + + o.HTTPRequest = r + + if runtime.HasBody(r) { + defer r.Body.Close() + var body ghcmessages.BulkAssignmentSavePayload + if err := route.Consumer.Consume(r.Body, &body); err != nil { + if err == io.EOF { + res = append(res, errors.Required("bulkAssignmentSavePayload", "body", "")) + } else { + res = append(res, errors.NewParseError("bulkAssignmentSavePayload", "body", "", err)) + } + } else { + // validate body object + if err := body.Validate(route.Formats); err != nil { + res = append(res, err) + } + + ctx := validate.WithOperationRequest(r.Context()) + if err := body.ContextValidate(ctx, route.Formats); err != nil { + res = append(res, err) + } + + if len(res) == 0 { + o.BulkAssignmentSavePayload = &body + } + } + } else { + res = append(res, errors.Required("bulkAssignmentSavePayload", "body", "")) + } + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/pkg/gen/ghcapi/ghcoperations/queues/save_bulk_assignment_data_responses.go b/pkg/gen/ghcapi/ghcoperations/queues/save_bulk_assignment_data_responses.go new file mode 100644 index 00000000000..99a269fdfda --- /dev/null +++ b/pkg/gen/ghcapi/ghcoperations/queues/save_bulk_assignment_data_responses.go @@ -0,0 +1,174 @@ +// 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" +) + +// SaveBulkAssignmentDataNoContentCode is the HTTP code returned for type SaveBulkAssignmentDataNoContent +const SaveBulkAssignmentDataNoContentCode int = 204 + +/* +SaveBulkAssignmentDataNoContent assigned + +swagger:response saveBulkAssignmentDataNoContent +*/ +type SaveBulkAssignmentDataNoContent struct { +} + +// NewSaveBulkAssignmentDataNoContent creates SaveBulkAssignmentDataNoContent with default headers values +func NewSaveBulkAssignmentDataNoContent() *SaveBulkAssignmentDataNoContent { + + return &SaveBulkAssignmentDataNoContent{} +} + +// WriteResponse to the client +func (o *SaveBulkAssignmentDataNoContent) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.Header().Del(runtime.HeaderContentType) //Remove Content-Type on empty responses + + rw.WriteHeader(204) +} + +// SaveBulkAssignmentDataUnauthorizedCode is the HTTP code returned for type SaveBulkAssignmentDataUnauthorized +const SaveBulkAssignmentDataUnauthorizedCode int = 401 + +/* +SaveBulkAssignmentDataUnauthorized The request was denied + +swagger:response saveBulkAssignmentDataUnauthorized +*/ +type SaveBulkAssignmentDataUnauthorized struct { + + /* + In: Body + */ + Payload *ghcmessages.Error `json:"body,omitempty"` +} + +// NewSaveBulkAssignmentDataUnauthorized creates SaveBulkAssignmentDataUnauthorized with default headers values +func NewSaveBulkAssignmentDataUnauthorized() *SaveBulkAssignmentDataUnauthorized { + + return &SaveBulkAssignmentDataUnauthorized{} +} + +// WithPayload adds the payload to the save bulk assignment data unauthorized response +func (o *SaveBulkAssignmentDataUnauthorized) WithPayload(payload *ghcmessages.Error) *SaveBulkAssignmentDataUnauthorized { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the save bulk assignment data unauthorized response +func (o *SaveBulkAssignmentDataUnauthorized) SetPayload(payload *ghcmessages.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *SaveBulkAssignmentDataUnauthorized) 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 + } + } +} + +// SaveBulkAssignmentDataNotFoundCode is the HTTP code returned for type SaveBulkAssignmentDataNotFound +const SaveBulkAssignmentDataNotFoundCode int = 404 + +/* +SaveBulkAssignmentDataNotFound The requested resource wasn't found + +swagger:response saveBulkAssignmentDataNotFound +*/ +type SaveBulkAssignmentDataNotFound struct { + + /* + In: Body + */ + Payload *ghcmessages.Error `json:"body,omitempty"` +} + +// NewSaveBulkAssignmentDataNotFound creates SaveBulkAssignmentDataNotFound with default headers values +func NewSaveBulkAssignmentDataNotFound() *SaveBulkAssignmentDataNotFound { + + return &SaveBulkAssignmentDataNotFound{} +} + +// WithPayload adds the payload to the save bulk assignment data not found response +func (o *SaveBulkAssignmentDataNotFound) WithPayload(payload *ghcmessages.Error) *SaveBulkAssignmentDataNotFound { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the save bulk assignment data not found response +func (o *SaveBulkAssignmentDataNotFound) SetPayload(payload *ghcmessages.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *SaveBulkAssignmentDataNotFound) 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 + } + } +} + +// SaveBulkAssignmentDataInternalServerErrorCode is the HTTP code returned for type SaveBulkAssignmentDataInternalServerError +const SaveBulkAssignmentDataInternalServerErrorCode int = 500 + +/* +SaveBulkAssignmentDataInternalServerError A server error occurred + +swagger:response saveBulkAssignmentDataInternalServerError +*/ +type SaveBulkAssignmentDataInternalServerError struct { + + /* + In: Body + */ + Payload *ghcmessages.Error `json:"body,omitempty"` +} + +// NewSaveBulkAssignmentDataInternalServerError creates SaveBulkAssignmentDataInternalServerError with default headers values +func NewSaveBulkAssignmentDataInternalServerError() *SaveBulkAssignmentDataInternalServerError { + + return &SaveBulkAssignmentDataInternalServerError{} +} + +// WithPayload adds the payload to the save bulk assignment data internal server error response +func (o *SaveBulkAssignmentDataInternalServerError) WithPayload(payload *ghcmessages.Error) *SaveBulkAssignmentDataInternalServerError { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the save bulk assignment data internal server error response +func (o *SaveBulkAssignmentDataInternalServerError) SetPayload(payload *ghcmessages.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *SaveBulkAssignmentDataInternalServerError) 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/save_bulk_assignment_data_urlbuilder.go b/pkg/gen/ghcapi/ghcoperations/queues/save_bulk_assignment_data_urlbuilder.go new file mode 100644 index 00000000000..d021ae6b61c --- /dev/null +++ b/pkg/gen/ghcapi/ghcoperations/queues/save_bulk_assignment_data_urlbuilder.go @@ -0,0 +1,87 @@ +// 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" +) + +// SaveBulkAssignmentDataURL generates an URL for the save bulk assignment data operation +type SaveBulkAssignmentDataURL struct { + _basePath string +} + +// 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 *SaveBulkAssignmentDataURL) WithBasePath(bp string) *SaveBulkAssignmentDataURL { + 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 *SaveBulkAssignmentDataURL) SetBasePath(bp string) { + o._basePath = bp +} + +// Build a url path and query string +func (o *SaveBulkAssignmentDataURL) Build() (*url.URL, error) { + var _result url.URL + + var _path = "/queues/bulk-assignment/assign" + + _basePath := o._basePath + if _basePath == "" { + _basePath = "/ghc/v1" + } + _result.Path = golangswaggerpaths.Join(_basePath, _path) + + return &_result, nil +} + +// Must is a helper function to panic when the url builder returns an error +func (o *SaveBulkAssignmentDataURL) 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 *SaveBulkAssignmentDataURL) String() string { + return o.Must(o.Build()).String() +} + +// BuildFull builds a full url with scheme, host, path and query string +func (o *SaveBulkAssignmentDataURL) BuildFull(scheme, host string) (*url.URL, error) { + if scheme == "" { + return nil, errors.New("scheme is required for a full url on SaveBulkAssignmentDataURL") + } + if host == "" { + return nil, errors.New("host is required for a full url on SaveBulkAssignmentDataURL") + } + + 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 *SaveBulkAssignmentDataURL) StringFull(scheme, host string) string { + return o.Must(o.BuildFull(scheme, host)).String() +} diff --git a/pkg/gen/ghcmessages/bulk_assignment_for_user.go b/pkg/gen/ghcmessages/bulk_assignment_for_user.go new file mode 100644 index 00000000000..7c0c46c1f31 --- /dev/null +++ b/pkg/gen/ghcmessages/bulk_assignment_for_user.go @@ -0,0 +1,77 @@ +// 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" + "github.com/go-openapi/validate" +) + +// BulkAssignmentForUser bulk assignment for user +// +// swagger:model BulkAssignmentForUser +type BulkAssignmentForUser struct { + + // id + // Format: uuid + ID strfmt.UUID `json:"id,omitempty"` + + // move assignments + MoveAssignments int64 `json:"moveAssignments"` +} + +// Validate validates this bulk assignment for user +func (m *BulkAssignmentForUser) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateID(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *BulkAssignmentForUser) validateID(formats strfmt.Registry) error { + if swag.IsZero(m.ID) { // not required + return nil + } + + if err := validate.FormatOf("id", "body", "uuid", m.ID.String(), formats); err != nil { + return err + } + + return nil +} + +// ContextValidate validates this bulk assignment for user based on context it is used +func (m *BulkAssignmentForUser) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (m *BulkAssignmentForUser) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *BulkAssignmentForUser) UnmarshalBinary(b []byte) error { + var res BulkAssignmentForUser + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/pkg/gen/ghcmessages/bulk_assignment_move_data.go b/pkg/gen/ghcmessages/bulk_assignment_move_data.go new file mode 100644 index 00000000000..c69c23c9ee7 --- /dev/null +++ b/pkg/gen/ghcmessages/bulk_assignment_move_data.go @@ -0,0 +1,38 @@ +// 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" +) + +// BulkAssignmentMoveData bulk assignment move data +// +// swagger:model BulkAssignmentMoveData +type BulkAssignmentMoveData strfmt.UUID + +// Validate validates this bulk assignment move data +func (m BulkAssignmentMoveData) 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 data based on context it is used +func (m BulkAssignmentMoveData) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} diff --git a/pkg/gen/ghcmessages/bulk_assignment_save_payload.go b/pkg/gen/ghcmessages/bulk_assignment_save_payload.go new file mode 100644 index 00000000000..376206bdf7c --- /dev/null +++ b/pkg/gen/ghcmessages/bulk_assignment_save_payload.go @@ -0,0 +1,233 @@ +// 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" + "encoding/json" + "strconv" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// BulkAssignmentSavePayload bulk assignment save payload +// +// swagger:model BulkAssignmentSavePayload +type BulkAssignmentSavePayload struct { + + // move data + MoveData []BulkAssignmentMoveData `json:"moveData"` + + // A string corresponding to the queue type + // Enum: [COUNSELING CLOSEOUT TASK_ORDER PAYMENT_REQUEST] + QueueType string `json:"queueType,omitempty"` + + // user data + UserData []*BulkAssignmentForUser `json:"userData"` +} + +// Validate validates this bulk assignment save payload +func (m *BulkAssignmentSavePayload) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateMoveData(formats); err != nil { + res = append(res, err) + } + + if err := m.validateQueueType(formats); err != nil { + res = append(res, err) + } + + if err := m.validateUserData(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *BulkAssignmentSavePayload) validateMoveData(formats strfmt.Registry) error { + if swag.IsZero(m.MoveData) { // not required + return nil + } + + for i := 0; i < len(m.MoveData); i++ { + + if err := m.MoveData[i].Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("moveData" + "." + strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("moveData" + "." + strconv.Itoa(i)) + } + return err + } + + } + + return nil +} + +var bulkAssignmentSavePayloadTypeQueueTypePropEnum []interface{} + +func init() { + var res []string + if err := json.Unmarshal([]byte(`["COUNSELING","CLOSEOUT","TASK_ORDER","PAYMENT_REQUEST"]`), &res); err != nil { + panic(err) + } + for _, v := range res { + bulkAssignmentSavePayloadTypeQueueTypePropEnum = append(bulkAssignmentSavePayloadTypeQueueTypePropEnum, v) + } +} + +const ( + + // BulkAssignmentSavePayloadQueueTypeCOUNSELING captures enum value "COUNSELING" + BulkAssignmentSavePayloadQueueTypeCOUNSELING string = "COUNSELING" + + // BulkAssignmentSavePayloadQueueTypeCLOSEOUT captures enum value "CLOSEOUT" + BulkAssignmentSavePayloadQueueTypeCLOSEOUT string = "CLOSEOUT" + + // BulkAssignmentSavePayloadQueueTypeTASKORDER captures enum value "TASK_ORDER" + BulkAssignmentSavePayloadQueueTypeTASKORDER string = "TASK_ORDER" + + // BulkAssignmentSavePayloadQueueTypePAYMENTREQUEST captures enum value "PAYMENT_REQUEST" + BulkAssignmentSavePayloadQueueTypePAYMENTREQUEST string = "PAYMENT_REQUEST" +) + +// prop value enum +func (m *BulkAssignmentSavePayload) validateQueueTypeEnum(path, location string, value string) error { + if err := validate.EnumCase(path, location, value, bulkAssignmentSavePayloadTypeQueueTypePropEnum, true); err != nil { + return err + } + return nil +} + +func (m *BulkAssignmentSavePayload) validateQueueType(formats strfmt.Registry) error { + if swag.IsZero(m.QueueType) { // not required + return nil + } + + // value enum + if err := m.validateQueueTypeEnum("queueType", "body", m.QueueType); err != nil { + return err + } + + return nil +} + +func (m *BulkAssignmentSavePayload) validateUserData(formats strfmt.Registry) error { + if swag.IsZero(m.UserData) { // not required + return nil + } + + for i := 0; i < len(m.UserData); i++ { + if swag.IsZero(m.UserData[i]) { // not required + continue + } + + if m.UserData[i] != nil { + if err := m.UserData[i].Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("userData" + "." + strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("userData" + "." + strconv.Itoa(i)) + } + return err + } + } + + } + + return nil +} + +// ContextValidate validate this bulk assignment save payload based on the context it is used +func (m *BulkAssignmentSavePayload) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateMoveData(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateUserData(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *BulkAssignmentSavePayload) contextValidateMoveData(ctx context.Context, formats strfmt.Registry) error { + + for i := 0; i < len(m.MoveData); i++ { + + if swag.IsZero(m.MoveData[i]) { // not required + return nil + } + + if err := m.MoveData[i].ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("moveData" + "." + strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("moveData" + "." + strconv.Itoa(i)) + } + return err + } + + } + + return nil +} + +func (m *BulkAssignmentSavePayload) contextValidateUserData(ctx context.Context, formats strfmt.Registry) error { + + for i := 0; i < len(m.UserData); i++ { + + if m.UserData[i] != nil { + + if swag.IsZero(m.UserData[i]) { // not required + return nil + } + + if err := m.UserData[i].ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("userData" + "." + strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("userData" + "." + strconv.Itoa(i)) + } + return err + } + } + + } + + return nil +} + +// MarshalBinary interface implementation +func (m *BulkAssignmentSavePayload) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *BulkAssignmentSavePayload) UnmarshalBinary(b []byte) error { + var res BulkAssignmentSavePayload + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/pkg/handlers/ghcapi/api.go b/pkg/handlers/ghcapi/api.go index 3bca03020cb..de468ebd102 100644 --- a/pkg/handlers/ghcapi/api.go +++ b/pkg/handlers/ghcapi/api.go @@ -575,6 +575,13 @@ func NewGhcAPIHandler(handlerConfig handlers.HandlerConfig) *ghcops.MymoveAPI { move.NewMoveFetcherBulkAssignment(), } + ghcAPI.QueuesSaveBulkAssignmentDataHandler = SaveBulkAssignmentDataHandler{ + handlerConfig, + officeusercreator.NewOfficeUserFetcherPop(), + move.NewMoveFetcher(), + move.NewMoveAssignerBulkAssignment(), + } + ghcAPI.QueuesGetMovesQueueHandler = GetMovesQueueHandler{ handlerConfig, order.NewOrderFetcher(waf), diff --git a/pkg/handlers/ghcapi/queues.go b/pkg/handlers/ghcapi/queues.go index 4d708d415b4..f85628ef8bc 100644 --- a/pkg/handlers/ghcapi/queues.go +++ b/pkg/handlers/ghcapi/queues.go @@ -816,6 +816,64 @@ func (h GetBulkAssignmentDataHandler) Handle( }) } +// SaveBulkAssignmentDataHandler saves the bulk assignment data +type SaveBulkAssignmentDataHandler struct { + handlers.HandlerConfig + services.OfficeUserFetcherPop + services.MoveFetcher + services.MoveAssigner +} + +func (h SaveBulkAssignmentDataHandler) Handle( + params queues.SaveBulkAssignmentDataParams, +) 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.NewSaveBulkAssignmentDataUnauthorized(), err + } + + officeUser, err := h.OfficeUserFetcherPop.FetchOfficeUserByID(appCtx, appCtx.Session().OfficeUserID) + if err != nil { + appCtx.Logger().Error("Error retrieving office_user", zap.Error(err)) + return queues.NewSaveBulkAssignmentDataNotFound(), err + } + + privileges, err := models.FetchPrivilegesForUser(appCtx.DB(), *officeUser.UserID) + if err != nil { + appCtx.Logger().Error("Error retreiving user privileges", zap.Error(err)) + return queues.NewSaveBulkAssignmentDataNotFound(), err + } + + isSupervisor := privileges.HasPrivilege(models.PrivilegeTypeSupervisor) + if !isSupervisor { + appCtx.Logger().Error("Unauthorized", zap.Error(err)) + return queues.NewSaveBulkAssignmentDataUnauthorized(), err + } + + queueType := params.BulkAssignmentSavePayload.QueueType + moveData := params.BulkAssignmentSavePayload.MoveData + userData := params.BulkAssignmentSavePayload.UserData + + // fetch the moves available to be assigned to their office users + movesForAssignment, err := h.MoveFetcher.FetchMovesByIdArray(appCtx, moveData) + if err != nil { + appCtx.Logger().Error("Error retreiving moves for assignment", zap.Error(err)) + return queues.NewSaveBulkAssignmentDataInternalServerError(), err + } + + _, err = h.MoveAssigner.BulkMoveAssignment(appCtx, queueType, userData, movesForAssignment) + if err != nil { + appCtx.Logger().Error("Error assigning moves", zap.Error(err)) + return queues.NewGetBulkAssignmentDataInternalServerError(), err + } + + return queues.NewSaveBulkAssignmentDataNoContent(), 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/handlers/ghcapi/queues_test.go b/pkg/handlers/ghcapi/queues_test.go index d3d39152f8c..386495d13b0 100644 --- a/pkg/handlers/ghcapi/queues_test.go +++ b/pkg/handlers/ghcapi/queues_test.go @@ -2252,7 +2252,138 @@ func (suite *HandlerSuite) TestAvailableOfficeUsers() { suite.Equal(subtestData.officeUsers[0].ID.String(), payload.QueuePaymentRequests[0].AvailableOfficeUsers[0].OfficeUserID.String()) suite.Equal(subtestData.officeUsers[1].ID.String(), payload.QueuePaymentRequests[0].AvailableOfficeUsers[1].OfficeUserID.String()) }) +} + +func (suite *HandlerSuite) TestSaveBulkAssignmentDataHandler() { + 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) + + transportationOffice := factory.BuildTransportationOffice(suite.DB(), nil, nil) + move := factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusNeedsServiceCounseling, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + }, nil) + + userData := []*ghcmessages.BulkAssignmentForUser{ + {ID: strfmt.UUID(officeUser.ID.String()), MoveAssignments: 1}, + } + moveData := []ghcmessages.BulkAssignmentMoveData{ghcmessages.BulkAssignmentMoveData(move.ID.String())} + + request := httptest.NewRequest("POST", "/queues/bulk-assignment/assign", nil) + request = suite.AuthenticateOfficeRequest(request, officeUser) + params := queues.SaveBulkAssignmentDataParams{ + HTTPRequest: request, + BulkAssignmentSavePayload: &ghcmessages.BulkAssignmentSavePayload{ + QueueType: "COUNSELING", + MoveData: moveData, + UserData: userData, + }, + } + handlerConfig := suite.HandlerConfig() + handler := SaveBulkAssignmentDataHandler{ + handlerConfig, + officeusercreator.NewOfficeUserFetcherPop(), + movefetcher.NewMoveFetcher(), + movefetcher.NewMoveAssignerBulkAssignment(), + } + response := handler.Handle(params) + suite.IsNotErrResponse(response) + suite.IsType(&queues.SaveBulkAssignmentDataUnauthorized{}, response) + }) + + suite.Run("successfully assigns bulk assignments", func() { + transportationOffice := factory.BuildTransportationOffice(suite.DB(), nil, nil) + + officeUser := factory.BuildOfficeUserWithPrivileges(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + Email: "officeuser1@example.com", + Active: true, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + { + Model: models.User{ + Privileges: []models.Privilege{ + { + PrivilegeType: models.PrivilegeTypeSupervisor, + }, + }, + Roles: []roles.Role{ + { + RoleType: roles.RoleTypeServicesCounselor, + }, + }, + }, + }, + }, nil) + move := factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusNeedsServiceCounseling, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + }, nil) + + userData := []*ghcmessages.BulkAssignmentForUser{ + {ID: strfmt.UUID(officeUser.ID.String()), MoveAssignments: 1}, + } + moveData := []ghcmessages.BulkAssignmentMoveData{ghcmessages.BulkAssignmentMoveData(move.ID.String())} + + request := httptest.NewRequest("POST", "/queues/bulk-assignment/assign", nil) + request = suite.AuthenticateOfficeRequest(request, officeUser) + params := queues.SaveBulkAssignmentDataParams{ + HTTPRequest: request, + BulkAssignmentSavePayload: &ghcmessages.BulkAssignmentSavePayload{ + QueueType: "COUNSELING", + MoveData: moveData, + UserData: userData, + }, + } + handlerConfig := suite.HandlerConfig() + handler := SaveBulkAssignmentDataHandler{ + handlerConfig, + officeusercreator.NewOfficeUserFetcherPop(), + movefetcher.NewMoveFetcher(), + movefetcher.NewMoveAssignerBulkAssignment(), + } + response := handler.Handle(params) + suite.IsNotErrResponse(response) + suite.IsType(&queues.SaveBulkAssignmentDataNoContent{}, response) + }) } func (suite *HandlerSuite) TestGetDestinationRequestsQueuesHandler() { diff --git a/pkg/services/mocks/MoveAssigner.go b/pkg/services/mocks/MoveAssigner.go new file mode 100644 index 00000000000..5aa23ae5697 --- /dev/null +++ b/pkg/services/mocks/MoveAssigner.go @@ -0,0 +1,61 @@ +// Code generated by mockery. DO NOT EDIT. + +package mocks + +import ( + appcontext "github.com/transcom/mymove/pkg/appcontext" + ghcmessages "github.com/transcom/mymove/pkg/gen/ghcmessages" + + mock "github.com/stretchr/testify/mock" + + models "github.com/transcom/mymove/pkg/models" +) + +// MoveAssigner is an autogenerated mock type for the MoveAssigner type +type MoveAssigner struct { + mock.Mock +} + +// BulkMoveAssignment provides a mock function with given fields: appCtx, queueType, officeUserData, movesToAssign +func (_m *MoveAssigner) BulkMoveAssignment(appCtx appcontext.AppContext, queueType string, officeUserData []*ghcmessages.BulkAssignmentForUser, movesToAssign models.Moves) (*models.Moves, error) { + ret := _m.Called(appCtx, queueType, officeUserData, movesToAssign) + + if len(ret) == 0 { + panic("no return value specified for BulkMoveAssignment") + } + + var r0 *models.Moves + var r1 error + if rf, ok := ret.Get(0).(func(appcontext.AppContext, string, []*ghcmessages.BulkAssignmentForUser, models.Moves) (*models.Moves, error)); ok { + return rf(appCtx, queueType, officeUserData, movesToAssign) + } + if rf, ok := ret.Get(0).(func(appcontext.AppContext, string, []*ghcmessages.BulkAssignmentForUser, models.Moves) *models.Moves); ok { + r0 = rf(appCtx, queueType, officeUserData, movesToAssign) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.Moves) + } + } + + if rf, ok := ret.Get(1).(func(appcontext.AppContext, string, []*ghcmessages.BulkAssignmentForUser, models.Moves) error); ok { + r1 = rf(appCtx, queueType, officeUserData, movesToAssign) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewMoveAssigner creates a new instance of MoveAssigner. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMoveAssigner(t interface { + mock.TestingT + Cleanup(func()) +}) *MoveAssigner { + mock := &MoveAssigner{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/services/mocks/MoveFetcher.go b/pkg/services/mocks/MoveFetcher.go index 22a26abbd68..454d022156b 100644 --- a/pkg/services/mocks/MoveFetcher.go +++ b/pkg/services/mocks/MoveFetcher.go @@ -3,8 +3,10 @@ package mocks import ( - mock "github.com/stretchr/testify/mock" appcontext "github.com/transcom/mymove/pkg/appcontext" + ghcmessages "github.com/transcom/mymove/pkg/gen/ghcmessages" + + mock "github.com/stretchr/testify/mock" models "github.com/transcom/mymove/pkg/models" @@ -46,6 +48,36 @@ func (_m *MoveFetcher) FetchMove(appCtx appcontext.AppContext, locator string, s return r0, r1 } +// FetchMovesByIdArray provides a mock function with given fields: appCtx, moveIds +func (_m *MoveFetcher) FetchMovesByIdArray(appCtx appcontext.AppContext, moveIds []ghcmessages.BulkAssignmentMoveData) (models.Moves, error) { + ret := _m.Called(appCtx, moveIds) + + if len(ret) == 0 { + panic("no return value specified for FetchMovesByIdArray") + } + + var r0 models.Moves + var r1 error + if rf, ok := ret.Get(0).(func(appcontext.AppContext, []ghcmessages.BulkAssignmentMoveData) (models.Moves, error)); ok { + return rf(appCtx, moveIds) + } + if rf, ok := ret.Get(0).(func(appcontext.AppContext, []ghcmessages.BulkAssignmentMoveData) models.Moves); ok { + r0 = rf(appCtx, moveIds) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(models.Moves) + } + } + + if rf, ok := ret.Get(1).(func(appcontext.AppContext, []ghcmessages.BulkAssignmentMoveData) error); ok { + r1 = rf(appCtx, moveIds) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // FetchMovesForPPTASReports provides a mock function with given fields: appCtx, params func (_m *MoveFetcher) FetchMovesForPPTASReports(appCtx appcontext.AppContext, params *services.MoveTaskOrderFetcherParams) (models.Moves, error) { ret := _m.Called(appCtx, params) diff --git a/pkg/services/move.go b/pkg/services/move.go index ff3abe681de..47792c5877b 100644 --- a/pkg/services/move.go +++ b/pkg/services/move.go @@ -8,6 +8,7 @@ import ( "github.com/gofrs/uuid" "github.com/transcom/mymove/pkg/appcontext" + "github.com/transcom/mymove/pkg/gen/ghcmessages" "github.com/transcom/mymove/pkg/models" "github.com/transcom/mymove/pkg/models/roles" "github.com/transcom/mymove/pkg/storage" @@ -27,6 +28,7 @@ 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) + FetchMovesByIdArray(appCtx appcontext.AppContext, moveIds []ghcmessages.BulkAssignmentMoveData) (models.Moves, error) } type MoveFetcherBulkAssignment interface { @@ -136,3 +138,10 @@ type MoveAssignedOfficeUserUpdater interface { type CheckForLockedMovesAndUnlockHandler interface { CheckForLockedMovesAndUnlock(appCtx appcontext.AppContext, officeUserID uuid.UUID) error } + +// MoveAssigner is the exported interface for bulk assigning moves to office users +// +//go:generate mockery --name MoveAssigner +type MoveAssigner interface { + BulkMoveAssignment(appCtx appcontext.AppContext, queueType string, officeUserData []*ghcmessages.BulkAssignmentForUser, movesToAssign models.Moves) (*models.Moves, error) +} diff --git a/pkg/services/move/move_assignment.go b/pkg/services/move/move_assignment.go new file mode 100644 index 00000000000..a55211f9e81 --- /dev/null +++ b/pkg/services/move/move_assignment.go @@ -0,0 +1,62 @@ +package move + +import ( + "github.com/gofrs/uuid" + + "github.com/transcom/mymove/pkg/appcontext" + "github.com/transcom/mymove/pkg/apperror" + "github.com/transcom/mymove/pkg/gen/ghcmessages" + "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/services" +) + +type moveAssigner struct { +} + +func NewMoveAssignerBulkAssignment() services.MoveAssigner { + return &moveAssigner{} +} + +func (a moveAssigner) BulkMoveAssignment(appCtx appcontext.AppContext, queueType string, officeUserData []*ghcmessages.BulkAssignmentForUser, movesToAssign models.Moves) (*models.Moves, error) { + if len(movesToAssign) == 0 { + return nil, apperror.NewBadDataError("No moves to assign") + } + + transactionErr := appCtx.NewTransaction(func(txnAppCtx appcontext.AppContext) error { + for _, move := range movesToAssign { + for _, officeUser := range officeUserData { + if officeUser != nil && officeUser.MoveAssignments > 0 { + officeUserId := uuid.FromStringOrNil(officeUser.ID.String()) + + switch queueType { + case string(models.QueueTypeCounseling): + move.SCAssignedID = &officeUserId + case string(models.QueueTypeCloseout): + move.SCAssignedID = &officeUserId + case string(models.QueueTypeTaskOrder): + move.TOOAssignedID = &officeUserId + case string(models.QueueTypePaymentRequest): + move.TIOAssignedID = &officeUserId + } + + officeUser.MoveAssignments -= 1 + + verrs, err := appCtx.DB().ValidateAndUpdate(&move) + if err != nil || verrs.HasAny() { + return apperror.NewInvalidInputError(move.ID, err, verrs, "") + } + + break + } + } + } + + return nil + }) + + if transactionErr != nil { + return nil, transactionErr + } + + return nil, nil +} diff --git a/pkg/services/move/move_assignment_test.go b/pkg/services/move/move_assignment_test.go new file mode 100644 index 00000000000..5c4ced099af --- /dev/null +++ b/pkg/services/move/move_assignment_test.go @@ -0,0 +1,252 @@ +package move + +import ( + "github.com/go-openapi/strfmt" + + "github.com/transcom/mymove/pkg/factory" + "github.com/transcom/mymove/pkg/gen/ghcmessages" + "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/models/roles" +) + +func (suite *MoveServiceSuite) TestBulkMoveAssignment() { + moveAssigner := NewMoveAssignerBulkAssignment() + + setupTestData := func() (models.TransportationOffice, models.Move, models.Move, models.Move) { + transportationOffice := factory.BuildTransportationOffice(suite.DB(), nil, nil) + move1 := factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusNeedsServiceCounseling, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + }, nil) + + move2 := factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusNeedsServiceCounseling, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + }, nil) + + move3 := factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusNeedsServiceCounseling, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + }, nil) + + return transportationOffice, move1, move2, move3 + } + + suite.Run("successfully assigns multiple counseling moves to a SC user", func() { + transportationOffice, move1, move2, move3 := setupTestData() + + officeUser := factory.BuildOfficeUserWithPrivileges(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + Email: "officeuser1@example.com", + Active: true, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + { + Model: models.User{ + Privileges: []models.Privilege{ + { + PrivilegeType: models.PrivilegeTypeSupervisor, + }, + }, + Roles: []roles.Role{ + { + RoleType: roles.RoleTypeServicesCounselor, + }, + }, + }, + }, + }, nil) + + moves := []models.Move{move1, move2, move3} + userData := []*ghcmessages.BulkAssignmentForUser{ + {ID: strfmt.UUID(officeUser.ID.String()), MoveAssignments: 2}, + } + + _, err := moveAssigner.BulkMoveAssignment(suite.AppContextForTest(), string(models.QueueTypeCounseling), userData, moves) + suite.NoError(err) + + // reload move data to check assigned + suite.NoError(suite.DB().Reload(&move1)) + suite.NoError(suite.DB().Reload(&move2)) + suite.NoError(suite.DB().Reload(&move3)) + + suite.Equal(officeUser.ID, *move1.SCAssignedID) + suite.Equal(officeUser.ID, *move2.SCAssignedID) + suite.Nil(move3.SCAssignedID) + }) + + suite.Run("successfully assigns multiple closeout moves to a SC user", func() { + transportationOffice, move1, move2, move3 := setupTestData() + + officeUser := factory.BuildOfficeUserWithPrivileges(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + Email: "officeuser1@example.com", + Active: true, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + { + Model: models.User{ + Privileges: []models.Privilege{ + { + PrivilegeType: models.PrivilegeTypeSupervisor, + }, + }, + Roles: []roles.Role{ + { + RoleType: roles.RoleTypeServicesCounselor, + }, + }, + }, + }, + }, nil) + + moves := []models.Move{move1, move2, move3} + userData := []*ghcmessages.BulkAssignmentForUser{ + {ID: strfmt.UUID(officeUser.ID.String()), MoveAssignments: 2}, + } + + _, err := moveAssigner.BulkMoveAssignment(suite.AppContextForTest(), string(models.QueueTypeCloseout), userData, moves) + suite.NoError(err) + + // reload move data to check assigned + suite.NoError(suite.DB().Reload(&move1)) + suite.NoError(suite.DB().Reload(&move2)) + suite.NoError(suite.DB().Reload(&move3)) + + suite.Equal(officeUser.ID, *move1.SCAssignedID) + suite.Equal(officeUser.ID, *move2.SCAssignedID) + suite.Nil(move3.SCAssignedID) + }) + + suite.Run("successfully assigns multiple task order moves to a TOO user", func() { + transportationOffice, move1, move2, move3 := setupTestData() + + officeUser := factory.BuildOfficeUserWithPrivileges(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + Email: "officeuser1@example.com", + Active: true, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + { + Model: models.User{ + Privileges: []models.Privilege{ + { + PrivilegeType: models.PrivilegeTypeSupervisor, + }, + }, + Roles: []roles.Role{ + { + RoleType: roles.RoleTypeTOO, + }, + }, + }, + }, + }, nil) + + moves := []models.Move{move1, move2, move3} + userData := []*ghcmessages.BulkAssignmentForUser{ + {ID: strfmt.UUID(officeUser.ID.String()), MoveAssignments: 2}, + } + + _, err := moveAssigner.BulkMoveAssignment(suite.AppContextForTest(), string(models.QueueTypeTaskOrder), userData, moves) + suite.NoError(err) + + // reload move data to check assigned + suite.NoError(suite.DB().Reload(&move1)) + suite.NoError(suite.DB().Reload(&move2)) + suite.NoError(suite.DB().Reload(&move3)) + + suite.Equal(officeUser.ID, *move1.TOOAssignedID) + suite.Equal(officeUser.ID, *move2.TOOAssignedID) + suite.Nil(move3.SCAssignedID) + }) + + suite.Run("successfully assigns payment requests to a TIO user", func() { + transportationOffice, move1, move2, move3 := setupTestData() + officeUser := factory.BuildOfficeUserWithPrivileges(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + Email: "officeuser1@example.com", + Active: true, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + { + Model: models.User{ + Privileges: []models.Privilege{ + { + PrivilegeType: models.PrivilegeTypeSupervisor, + }, + }, + Roles: []roles.Role{ + { + RoleType: roles.RoleTypeTIO, + }, + }, + }, + }, + }, nil) + + moves := []models.Move{move1, move2, move3} + userData := []*ghcmessages.BulkAssignmentForUser{ + {ID: strfmt.UUID(officeUser.ID.String()), MoveAssignments: 2}, + } + + _, err := moveAssigner.BulkMoveAssignment(suite.AppContextForTest(), string(models.QueueTypePaymentRequest), userData, moves) + suite.NoError(err) + + // reload move data to check assigned + suite.NoError(suite.DB().Reload(&move1)) + suite.NoError(suite.DB().Reload(&move2)) + + suite.Equal(officeUser.ID, *move1.TIOAssignedID) + suite.Equal(officeUser.ID, *move2.TIOAssignedID) + suite.Nil(move3.TIOAssignedID) + }) +} diff --git a/pkg/services/move/move_fetcher.go b/pkg/services/move/move_fetcher.go index e0d6aea4f8a..e38da9e6000 100644 --- a/pkg/services/move/move_fetcher.go +++ b/pkg/services/move/move_fetcher.go @@ -9,6 +9,7 @@ import ( "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/ghcmessages" "github.com/transcom/mymove/pkg/gen/internalmessages" "github.com/transcom/mymove/pkg/models" "github.com/transcom/mymove/pkg/services" @@ -62,6 +63,21 @@ func (f moveFetcher) FetchMove(appCtx appcontext.AppContext, locator string, sea return move, nil } +func (f moveFetcher) FetchMovesByIdArray(appCtx appcontext.AppContext, moveIds []ghcmessages.BulkAssignmentMoveData) (models.Moves, error) { + moves := models.Moves{} + + err := appCtx.DB().Q(). + Where("id in (?)", moveIds). + Where("show = TRUE"). + All(&moves) + + if err != nil { + return nil, err + } + + return moves, nil +} + // Fetches moves for Navy servicemembers with approved shipments. Ignores gbloc rules func (f moveFetcher) FetchMovesForPPTASReports(appCtx appcontext.AppContext, params *services.MoveTaskOrderFetcherParams) (models.Moves, error) { var moves models.Moves @@ -172,7 +188,7 @@ func (f moveFetcherBulkAssignment) FetchMovesForBulkAssignmentCloseout(appCtx ap query := `SELECT moves.id, - ppm_shipments.submitted_at AS earliest_date + COALESCE(MIN(ppm_shipments.submitted_at), '0001-01-01') AS earliest_date FROM moves INNER JOIN orders ON orders.id = moves.orders_id INNER JOIN service_members ON service_members.id = orders.service_member_id @@ -200,7 +216,7 @@ func (f moveFetcherBulkAssignment) FetchMovesForBulkAssignmentCloseout(appCtx ap query += ` AND (ppm_shipments.status IN ($2)) AND (orders.orders_type NOT IN ($3, $4, $5)) - GROUP BY moves.id, ppm_shipments.submitted_at + GROUP BY moves.id ORDER BY earliest_date ASC` err := appCtx.DB().RawQuery(query, @@ -283,7 +299,7 @@ func (f moveFetcherBulkAssignment) FetchMovesForBulkAssignmentPaymentRequest(app sqlQuery := ` SELECT moves.id, - min(payment_requests.requested_at) AS earliest_date + MIN(payment_requests.requested_at) AS earliest_date FROM moves INNER JOIN orders ON orders.id = moves.orders_id INNER JOIN service_members ON orders.service_member_id = service_members.id diff --git a/src/components/BulkAssignment/BulkAssignmentModal.jsx b/src/components/BulkAssignment/BulkAssignmentModal.jsx index 9b84c51b4ce..a1732e44572 100644 --- a/src/components/BulkAssignment/BulkAssignmentModal.jsx +++ b/src/components/BulkAssignment/BulkAssignmentModal.jsx @@ -1,6 +1,8 @@ import React, { useCallback, useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { Button } from '@trussworks/react-uswds'; +import { Formik } from 'formik'; +import * as Yup from 'yup'; import styles from './BulkAssignmentModal.module.scss'; @@ -8,16 +10,39 @@ import Modal, { ModalTitle, ModalClose, ModalActions, connectModal } from 'compo import { getBulkAssignmentData } from 'services/ghcApi'; import { milmoveLogger } from 'utils/milmoveLog'; import { userName } from 'utils/formatters'; +import { Form } from 'components/form'; + +const initialValues = { + userData: [], + moveData: [], +}; export const BulkAssignmentModal = ({ onClose, onSubmit, title, submitText, closeText, queueType }) => { - const [showCancelModal, setShowCancelModal] = useState(false); + const [isError, setIsError] = useState(false); const [bulkAssignmentData, setBulkAssignmentData] = useState(null); const [isDisabled, setIsDisabled] = useState(false); const [numberOfMoves, setNumberOfMoves] = useState(0); + const [showCancelModal, setShowCancelModal] = useState(false); + + const errorMessage = 'Cannot assign more moves than are available.'; + + const initUserData = (availableOfficeUsers) => { + const officeUsers = []; + availableOfficeUsers.forEach((user) => { + const newUserAssignment = { + ID: user.officeUserId, + moveAssignments: 0, + }; + officeUsers.push(newUserAssignment); + }); + initialValues.userData = officeUsers; + }; + const fetchData = useCallback(async () => { try { const data = await getBulkAssignmentData(queueType); setBulkAssignmentData(data); + initUserData(data?.availableOfficeUsers); if (!data.bulkAssignmentMoveIDs) { setIsDisabled(true); @@ -34,6 +59,12 @@ export const BulkAssignmentModal = ({ onClose, onSubmit, title, submitText, clos fetchData(); }, [fetchData]); + initialValues.moveData = bulkAssignmentData?.bulkAssignmentMoveIDs; + + const validationSchema = Yup.object().shape({ + assignment: Yup.number().min(0).typeError('Assignment must be a number'), + }); + return (
@@ -44,67 +75,129 @@ export const BulkAssignmentModal = ({ onClose, onSubmit, title, submitText, clos
- - - - - - - {bulkAssignmentData?.availableOfficeUsers?.map((user) => { + { + const totalAssignment = values?.userData?.reduce((sum, item) => sum + item.moveAssignments, 0); + + if (totalAssignment > numberOfMoves) { + setIsError(true); + return; + } + + const bulkAssignmentSavePayload = values; + onSubmit({ bulkAssignmentSavePayload }); + onClose(); + }} + validationSchema={validationSchema} + initialValues={initialValues} + > + {({ handleChange, setValues, values }) => { + const handleAssignmentChange = (event, i) => { + handleChange(event); + setIsError(false); + + let newUserAssignment; + if (event.target.value !== '') { + newUserAssignment = { + ID: event.target.id, + moveAssignments: +event.target.value, + }; + } else { + newUserAssignment = { + ID: event.target.id, + moveAssignments: 0, + }; + } + + const newValues = values; + newValues.userData[i] = newUserAssignment; + + setValues({ + ...values, + userData: newValues.userData, + }); + }; + return ( - - - - - + +
UserWorkloadAssignment
-

{userName(user)}

-
-

{user.workload || 0}

-
- -
+ + + + + + {bulkAssignmentData?.availableOfficeUsers?.map((user, i) => { + return ( + + + + + + ); + })} +
UserWorkloadAssignment
+

+ {userName(user)} +

+
+

{user.workload || 0}

+
+ handleAssignmentChange(event, i)} + /> +
+ {showCancelModal ? ( +
+ + Any unsaved work will be lost. Are you sure you want to cancel? + +
+ + +
+
+ ) : ( + + + + {isError &&
{errorMessage}
} +
+ )} + ); - })} - + }} +
- {showCancelModal ? ( -
- Any unsaved work will be lost. Are you sure you want to cancel? -
- - -
-
- ) : ( - - - - - )}
); diff --git a/src/components/BulkAssignment/BulkAssignmentModal.module.scss b/src/components/BulkAssignment/BulkAssignmentModal.module.scss index 4fa9fd266a5..2350d09a756 100644 --- a/src/components/BulkAssignment/BulkAssignmentModal.module.scss +++ b/src/components/BulkAssignment/BulkAssignmentModal.module.scss @@ -2,7 +2,7 @@ .BulkModal { min-width: 650px !important; - max-width: 90vw; + max-width: 60vw; overflow-y: auto; max-height: 90vh; overflow-x: hidden; @@ -29,10 +29,22 @@ height: 42px; width: 68px; } + + .errorMessage { + margin: 0 auto; + display: flex; + text-align: center; + align-items: center; + color: $error; + font-size: large; + } } .BulkAssignmentTable { table { + table-layout: fixed; + width: 100%; + th { max-width: 10px; text-align: center; @@ -46,6 +58,10 @@ padding-left: 15px; padding-top: 4px; } + .officeUserFormattedName { + overflow: hidden; + text-overflow: ellipsis; + } } } diff --git a/src/components/BulkAssignment/BulkAssignmentModal.test.jsx b/src/components/BulkAssignment/BulkAssignmentModal.test.jsx index d1cf48141ef..8d6a77cdd17 100644 --- a/src/components/BulkAssignment/BulkAssignmentModal.test.jsx +++ b/src/components/BulkAssignment/BulkAssignmentModal.test.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import { act, render, screen } from '@testing-library/react'; +import { act, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { BulkAssignmentModal } from 'components/BulkAssignment/BulkAssignmentModal'; @@ -89,6 +89,47 @@ describe('BulkAssignmentModal', () => { expect(screen.getAllByTestId('bulkAssignmentUserWorkload')[0]).toHaveTextContent('1'); }); + it('submits the bulk assignment data', async () => { + render(); + const userTable = await screen.findByRole('table'); + expect(userTable).toBeInTheDocument(); + expect(screen.getByText('User')).toBeInTheDocument(); + expect(screen.getByText('Workload')).toBeInTheDocument(); + expect(screen.getByText('Assignment')).toBeInTheDocument(); + await act(async () => { + expect(await screen.getByText('user, sc')).toBeInTheDocument(); + const assignment = await screen.getAllByTestId('assignment')[0]; + await userEvent.type(assignment, '1'); + }); + expect(screen.getAllByTestId('bulkAssignmentUserWorkload')[0]).toHaveTextContent('1'); + + const saveButton = await screen.getByTestId('modalSubmitButton'); + await userEvent.click(saveButton); + await waitFor(() => { + const payload = { + bulkAssignmentSavePayload: { + moveData: [ + 'b3baf6ce-f43b-437c-85be-e1145c0ddb96', + '962ce8d2-03a2-435c-94ca-6b9ef6c226c1', + 'fee7f916-35a6-4c0b-9ea6-a1d8094b3ed3', + ], + userData: [ + { + ID: '045c3048-df9a-4d44-88ed-8cd6e2100e08', + moveAssignments: 1, + }, + { + ID: '4b1f2722-b0bf-4b16-b8c4-49b4e49ba42a', + moveAssignments: 0, + }, + ], + }, + }; + + expect(onSubmit).toHaveBeenCalledWith(payload); + }); + }); + it('closes the modal when the close is confirmed', async () => { render(); @@ -99,7 +140,7 @@ describe('BulkAssignmentModal', () => { const confirmButton = await screen.findByTestId('cancelModalYes'); await userEvent.click(confirmButton); - expect(onClose).toHaveBeenCalledTimes(1); + expect(onClose).toHaveBeenCalledTimes(2); }); it('close confirmation goes away when clicking no', async () => { diff --git a/src/components/Table/TableQueue.jsx b/src/components/Table/TableQueue.jsx index dae4e94bbda..acfe62db2c1 100644 --- a/src/components/Table/TableQueue.jsx +++ b/src/components/Table/TableQueue.jsx @@ -1,8 +1,9 @@ import React, { useState, useEffect, useMemo, useContext } from 'react'; import { connect } from 'react-redux'; -import { GridContainer, Button } from '@trussworks/react-uswds'; +import { GridContainer, Button, Grid, Alert } from '@trussworks/react-uswds'; import { useTable, useFilters, usePagination, useSortBy } from 'react-table'; import PropTypes from 'prop-types'; +import { useMutation } from '@tanstack/react-query'; import styles from './TableQueue.module.scss'; import TableCSVExportButton from './TableCSVExportButton'; @@ -27,6 +28,8 @@ import { getSelectionOptionLabel, } from 'components/Table/utils'; import { roleTypes } from 'constants/userRoles'; +import { saveBulkAssignmentData } from 'services/ghcApi'; +import { setRefetchQueue as setRefetchQueueAction } from 'store/general/actions'; const defaultPageSize = 20; const defaultPage = 1; @@ -57,6 +60,8 @@ const TableQueue = ({ officeUser, activeRole, queueType, + refetchQueue, + setRefetchQueue, }) => { const [isPageReload, setIsPageReload] = useState(true); useEffect(() => { @@ -107,9 +112,6 @@ const TableQueue = ({ setIsBulkAssignModalVisible(true); }; - const handleCloseBulkAssignModal = () => { - setIsBulkAssignModalVisible(false); - }; const { queueResult: { totalCount = 0, @@ -119,6 +121,7 @@ const TableQueue = ({ }, isInitialLoading: isLoading, isError, + refetch, } = useQueries({ sort: id, order: desc ? 'desc' : 'asc', @@ -128,6 +131,7 @@ const TableQueue = ({ viewAsGBLOC: selectedGbloc, activeRole, }); + const tableData = useMemo(() => data, [data]); // react-table setup below const defaultColumn = useMemo( @@ -137,7 +141,23 @@ const TableQueue = ({ }), [], ); - const tableData = useMemo(() => data, [data]); + + const [successMessageEnabled, setSuccessMessageEnabled] = useState(false); + + const { mutate: mutateBulkAssignment } = useMutation(saveBulkAssignmentData, { + onSuccess: async () => { + await refetch(); + + setSuccessMessageEnabled(true); + }, + }); + + useEffect(() => { + if (refetchQueue) + refetch().then(() => { + setRefetchQueue(false); + }); + }, [refetch, setRefetchQueue, refetchQueue]); const tableColumns = useMemo(() => columns, [columns]); const { @@ -317,12 +337,29 @@ const TableQueue = ({ return ''; }; + const handleCloseBulkAssignModal = () => { + setIsBulkAssignModalVisible(false); + }; + + const onSubmitBulk = (bulkAssignmentSavePayload) => { + mutateBulkAssignment({ queueType, ...bulkAssignmentSavePayload }); + }; + return (
+ {successMessageEnabled && ( + + + Moves assigned successfully + + + )} +
{isBulkAssignModalVisible && ( @@ -446,7 +483,10 @@ const mapStateToProps = (state) => { return { officeUser: user?.office_user || {}, activeRole: state.auth.activeRole, + refetchQueue: state?.generalState?.refetchQueue || false, }; }; -export default connect(mapStateToProps)(TableQueue); +const mapDispatchToProps = { setRefetchQueue: setRefetchQueueAction }; + +export default connect(mapStateToProps, mapDispatchToProps)(TableQueue); diff --git a/src/constants/MoveHistory/EventTemplates/SaveBulkAssignmentData/SaveBulkAssignmentData.jsx b/src/constants/MoveHistory/EventTemplates/SaveBulkAssignmentData/SaveBulkAssignmentData.jsx new file mode 100644 index 00000000000..2f4ffcb8e4c --- /dev/null +++ b/src/constants/MoveHistory/EventTemplates/SaveBulkAssignmentData/SaveBulkAssignmentData.jsx @@ -0,0 +1,23 @@ +import React from 'react'; + +import o from 'constants/MoveHistory/UIDisplay/Operations'; +import a from 'constants/MoveHistory/Database/Actions'; +import t from 'constants/MoveHistory/Database/Tables'; +import LabeledDetails from 'pages/Office/MoveHistory/LabeledDetails'; +import { formatAssignedOfficeUserFromContext } from 'utils/formatters'; + +const formatChangedValues = (historyRecord) => { + const newChangedValues = { + ...formatAssignedOfficeUserFromContext(historyRecord), + }; + + return { ...historyRecord, changedValues: newChangedValues }; +}; + +export default { + action: a.UPDATE, + eventName: o.saveBulkAssignmentData, + tableName: t.moves, + getEventNameDisplay: () => 'Updated move', + getDetails: (historyRecord) => , +}; diff --git a/src/constants/MoveHistory/EventTemplates/SaveBulkAssignmentData/SaveBulkAssignmentData.test.jsx b/src/constants/MoveHistory/EventTemplates/SaveBulkAssignmentData/SaveBulkAssignmentData.test.jsx new file mode 100644 index 00000000000..aeeed7586ac --- /dev/null +++ b/src/constants/MoveHistory/EventTemplates/SaveBulkAssignmentData/SaveBulkAssignmentData.test.jsx @@ -0,0 +1,65 @@ +import { screen, render } from '@testing-library/react'; + +import e from 'constants/MoveHistory/EventTemplates/SaveBulkAssignmentData/SaveBulkAssignmentData'; +import getTemplate from 'constants/MoveHistory/TemplateManager'; + +describe('When given a move that has been assigned', () => { + const historyRecord = { + action: 'UPDATE', + eventName: 'saveBulkAssignmentData', + tableName: 'moves', + changedValues: { + sc_assigned_id: 'fb625e3c-067c-49d7-8fd9-88ef040e6137', + }, + oldValues: { + sc_assigned_id: null, + }, + context: [{ assigned_office_user_last_name: 'Daniels', assigned_office_user_first_name: 'Jayden' }], + }; + + it('correctly matches the template', () => { + const template = getTemplate(historyRecord); + expect(template).toMatchObject(e); + }); + + it('displays the proper name in the event name display column', () => { + const template = getTemplate(historyRecord); + + render(template.getEventNameDisplay(historyRecord)); + expect(screen.getByText('Updated move')).toBeInTheDocument(); + }); + + describe('displays the proper details for', () => { + it('services counselor', () => { + const template = getTemplate(historyRecord); + + render(template.getDetails(historyRecord)); + expect(screen.getByText('Closeout counselor assigned')).toBeInTheDocument(); + expect(screen.getByText(': Daniels, Jayden')).toBeInTheDocument(); + }); + it('task ordering officer', () => { + historyRecord.changedValues = { too_assigned_id: 'fb625e3c-067c-49d7-8fd9-88ef040e6137' }; + historyRecord.oldValues = { too_assigned_id: null }; + historyRecord.context = [ + { assigned_office_user_last_name: 'Robinson', assigned_office_user_first_name: 'Brian' }, + ]; + + const template = getTemplate(historyRecord); + + render(template.getDetails(historyRecord)); + expect(screen.getByText('Task ordering officer assigned')).toBeInTheDocument(); + expect(screen.getByText(': Robinson, Brian')).toBeInTheDocument(); + }); + it('task invoicing officer', () => { + historyRecord.changedValues = { tio_assigned_id: 'fb625e3c-067c-49d7-8fd9-88ef040e6137' }; + historyRecord.oldValues = { tio_assigned_id: null }; + historyRecord.context = [{ assigned_office_user_last_name: 'Luvu', assigned_office_user_first_name: 'Frankie' }]; + + const template = getTemplate(historyRecord); + + render(template.getDetails(historyRecord)); + expect(screen.getByText('Task invoicing officer assigned')).toBeInTheDocument(); + expect(screen.getByText(': Luvu, Frankie')).toBeInTheDocument(); + }); + }); +}); diff --git a/src/constants/MoveHistory/EventTemplates/index.js b/src/constants/MoveHistory/EventTemplates/index.js index 68760edcd43..46404692b7c 100644 --- a/src/constants/MoveHistory/EventTemplates/index.js +++ b/src/constants/MoveHistory/EventTemplates/index.js @@ -116,8 +116,9 @@ export { default as deleteAssignedOfficeUser } from './UpdateAssignedOfficeUser/ export { default as UpdatePaymentRequestStatusMoves } from './UpdatePaymentRequestStatus/UpdatePaymentRequestStatusMoves'; export { default as reviewShipmentAddressUpdate } from './ReviewShipmentAddressUpdate/reviewShipmentAddressUpdate'; export { default as FinishDocumentReviewMoves } from './FinishDocumentReview/FinishDocumentReviewMoves'; +export { default as saveBulkAssignmentData } from './SaveBulkAssignmentData/SaveBulkAssignmentData'; export { default as approveShipments } from './ApproveShipments/approveShipments'; +export { default as updatePaymentServiceItemStatus } from './UpdatePaymentServiceItem/UpdatePaymentServiceItemStatus'; export { default as approveShipmentsUpdateAllowances } from './ApproveShipments/approveShipmentsUpdateAllowances'; export { default as approveShipmentsApproveMove } from './ApproveShipments/approveShipmentsApproveMove'; export { default as approveShipmentsServiceItem } from './ApproveShipments/approveShipmentsServiceItem'; -export { default as updatePaymentServiceItemStatus } from './UpdatePaymentServiceItem/UpdatePaymentServiceItemStatus'; diff --git a/src/constants/MoveHistory/UIDisplay/Operations.js b/src/constants/MoveHistory/UIDisplay/Operations.js index bbe9d846345..1df6d7d278b 100644 --- a/src/constants/MoveHistory/UIDisplay/Operations.js +++ b/src/constants/MoveHistory/UIDisplay/Operations.js @@ -68,7 +68,8 @@ export default { addAppealToViolation: 'addAppealToViolation', // ghc.yaml addAppealToSeriousIncident: 'addAppealToSeriousIncident', // ghc.yaml cancelMove: 'cancelMove', // internal.yaml + reviewShipmentAddressUpdate: 'reviewShipmentAddressUpdate', // ghc.yaml updateAssignedOfficeUser: 'updateAssignedOfficeUser', // ghc.yaml deleteAssignedOfficeUser: 'deleteAssignedOfficeUser', // ghc.yaml - reviewShipmentAddressUpdate: 'reviewShipmentAddressUpdate', // ghc.yaml + saveBulkAssignmentData: 'saveBulkAssignmentData', // ghc.yaml }; diff --git a/src/hooks/queries.js b/src/hooks/queries.js index 1129a3e7901..c49e9dc2310 100644 --- a/src/hooks/queries.js +++ b/src/hooks/queries.js @@ -574,7 +574,11 @@ export const useMovesQueueQueries = ({ viewAsGBLOC, activeRole, }) => { - const { data = {}, ...movesQueueQuery } = useQuery( + const { + refetch, + data = {}, + ...movesQueueQuery + } = useQuery( [MOVES_QUEUE, { sort, order, filters, currentPage, currentPageSize, viewAsGBLOC, activeRole }], ({ queryKey }) => getMovesQueue(...queryKey), ); @@ -585,6 +589,7 @@ export const useMovesQueueQueries = ({ isLoading, isError, isSuccess, + refetch, }; }; @@ -619,7 +624,11 @@ export const useServicesCounselingQueuePPMQueries = ({ viewAsGBLOC, activeRole, }) => { - const { data = {}, ...servicesCounselingQueueQuery } = useQuery( + const { + refetch, + data = {}, + ...servicesCounselingQueueQuery + } = useQuery( [ SERVICES_COUNSELING_QUEUE, { sort, order, filters, currentPage, currentPageSize, needsPPMCloseout: true, viewAsGBLOC, activeRole }, @@ -634,6 +643,7 @@ export const useServicesCounselingQueuePPMQueries = ({ isLoading, isError, isSuccess, + refetch, }; }; @@ -646,7 +656,11 @@ export const useServicesCounselingQueueQueries = ({ viewAsGBLOC, activeRole, }) => { - const { data = {}, ...servicesCounselingQueueQuery } = useQuery( + const { + refetch, + data = {}, + ...servicesCounselingQueueQuery + } = useQuery( [ SERVICES_COUNSELING_QUEUE, { sort, order, filters, currentPage, currentPageSize, needsPPMCloseout: false, viewAsGBLOC, activeRole }, @@ -661,6 +675,7 @@ export const useServicesCounselingQueueQueries = ({ isLoading, isError, isSuccess, + refetch, }; }; @@ -673,7 +688,11 @@ export const usePaymentRequestQueueQueries = ({ viewAsGBLOC, activeRole, }) => { - const { data = {}, ...paymentRequestsQueueQuery } = useQuery( + const { + refetch, + data = {}, + ...paymentRequestsQueueQuery + } = useQuery( [PAYMENT_REQUESTS_QUEUE, { sort, order, filters, currentPage, currentPageSize, viewAsGBLOC, activeRole }], ({ queryKey }) => getPaymentRequestsQueue(...queryKey), ); @@ -685,6 +704,7 @@ export const usePaymentRequestQueueQueries = ({ isLoading, isError, isSuccess, + refetch, }; }; diff --git a/src/hooks/queries.test.jsx b/src/hooks/queries.test.jsx index 86679de027e..fdcc5008a15 100644 --- a/src/hooks/queries.test.jsx +++ b/src/hooks/queries.test.jsx @@ -916,6 +916,7 @@ describe('useMovesQueueQueries', () => { isLoading: false, isError: false, isSuccess: true, + refetch: result.current.refetch, }); }); }); @@ -946,6 +947,7 @@ describe('usePaymentRequestsQueueQueries', () => { isLoading: false, isError: false, isSuccess: true, + refetch: result.current.refetch, }); }); }); @@ -1028,6 +1030,7 @@ describe('useServicesCounselingQueuePPMQueries', () => { isLoading: false, isError: false, isSuccess: true, + refetch: result.current.refetch, }); }); }); diff --git a/src/pages/Office/HeadquartersQueues/HeadquartersQueues.jsx b/src/pages/Office/HeadquartersQueues/HeadquartersQueues.jsx index c9d08481f3b..f99f6002a85 100644 --- a/src/pages/Office/HeadquartersQueues/HeadquartersQueues.jsx +++ b/src/pages/Office/HeadquartersQueues/HeadquartersQueues.jsx @@ -237,7 +237,7 @@ const HeadquartersQueue = ({ isQueueManagementFFEnabled, activeRole }) => { defaultSortedColumns={[{ id: 'status', desc: false }]} disableMultiSort disableSortBy={false} - columns={tooQueueColumns(moveLockFlag, isQueueManagementFFEnabled, showBranchFilter)} + columns={tooQueueColumns(moveLockFlag, isQueueManagementFFEnabled, null, showBranchFilter)} title="All moves" handleClick={handleClickNavigateToDetails} useQueries={useMovesQueueQueries} @@ -264,7 +264,7 @@ const HeadquartersQueue = ({ isQueueManagementFFEnabled, activeRole }) => { defaultSortedColumns={[{ id: 'age', desc: true }]} disableMultiSort disableSortBy={false} - columns={tioQueueColumns(moveLockFlag, isQueueManagementFFEnabled, showBranchFilter)} + columns={tioQueueColumns(moveLockFlag, isQueueManagementFFEnabled, null, showBranchFilter)} title="Payment requests" handleClick={handleClickNavigateToPaymentRequests} useQueries={usePaymentRequestQueueQueries} @@ -291,7 +291,7 @@ const HeadquartersQueue = ({ isQueueManagementFFEnabled, activeRole }) => { defaultSortedColumns={[{ id: 'closeoutInitiated', desc: false }]} disableMultiSort disableSortBy={false} - columns={closeoutColumns(moveLockFlag, inPPMCloseoutGBLOC, null, null, isQueueManagementFFEnabled)} + columns={closeoutColumns(moveLockFlag, inPPMCloseoutGBLOC, null, null, isQueueManagementFFEnabled, null)} title="Moves" handleClick={handleClickNavigateToDetails} useQueries={useServicesCounselingQueuePPMQueries} @@ -319,7 +319,7 @@ const HeadquartersQueue = ({ isQueueManagementFFEnabled, activeRole }) => { defaultSortedColumns={[{ id: 'submittedAt', desc: false }]} disableMultiSort disableSortBy={false} - columns={counselingColumns(moveLockFlag, null, null, isQueueManagementFFEnabled)} + columns={counselingColumns(moveLockFlag, null, null, isQueueManagementFFEnabled, null)} title="Moves" handleClick={handleClickNavigateToDetails} useQueries={useServicesCounselingQueueQueries} diff --git a/src/pages/Office/MoveQueue/MoveQueue.jsx b/src/pages/Office/MoveQueue/MoveQueue.jsx index e1cfdff3f35..6a58d40b9a9 100644 --- a/src/pages/Office/MoveQueue/MoveQueue.jsx +++ b/src/pages/Office/MoveQueue/MoveQueue.jsx @@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import { useNavigate, NavLink, useParams, Navigate, generatePath } from 'react-router-dom'; import { Dropdown } from '@trussworks/react-uswds'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { connect } from 'react-redux'; import styles from './MoveQueue.module.scss'; @@ -33,8 +34,9 @@ import NotFound from 'components/NotFound/NotFound'; import { isBooleanFlagEnabled } from 'utils/featureFlags'; import handleQueueAssignment from 'utils/queues'; import { elevatedPrivilegeTypes } from 'constants/userPrivileges'; +import { setRefetchQueue as setRefetchQueueAction } from 'store/general/actions'; -export const columns = (moveLockFlag, isQueueManagementEnabled, showBranchFilter = true) => { +export const columns = (moveLockFlag, isQueueManagementEnabled, setRefetchQueue, showBranchFilter = true) => { const cols = [ createHeader('ID', 'id', { id: 'id' }), createHeader( @@ -167,11 +169,14 @@ export const columns = (moveLockFlag, isQueueManagementEnabled, showBranchFilter
handleQueueAssignment(row.id, e.target.value, roleTypes.TOO)} + onChange={(e) => { + handleQueueAssignment(row.id, e.target.value, roleTypes.TOO); + setRefetchQueue(true); + }} title="Assigned dropdown" > - {row.availableOfficeUsers?.map(({ lastName, firstName, officeUserId }) => ( + {row.availableOfficeUsers.map(({ lastName, firstName, officeUserId }) => (