diff --git a/pkg/gen/ghcapi/embedded_spec.go b/pkg/gen/ghcapi/embedded_spec.go index 740d92c587f..d63a2540709 100644 --- a/pkg/gen/ghcapi/embedded_spec.go +++ b/pkg/gen/ghcapi/embedded_spec.go @@ -4535,7 +4535,8 @@ func init() { "dodID", "emplid", "age", - "originDutyLocation" + "originDutyLocation", + "assignedTo" ], "type": "string", "description": "field that results should be sorted by", @@ -4606,6 +4607,12 @@ func init() { "name": "originDutyLocation", "in": "query" }, + { + "type": "string", + "description": "Used to illustrate which user is assigned to this payment request.\n", + "name": "assignedTo", + "in": "query" + }, { "uniqueItems": true, "type": "array", @@ -11898,6 +11905,13 @@ func init() { "type": "number", "format": "double" }, + "assignable": { + "type": "boolean" + }, + "assignedTo": { + "x-nullable": true, + "$ref": "#/definitions/AssignedOfficeUser" + }, "availableOfficeUsers": { "$ref": "#/definitions/AvailableOfficeUsers" }, @@ -19899,7 +19913,8 @@ func init() { "dodID", "emplid", "age", - "originDutyLocation" + "originDutyLocation", + "assignedTo" ], "type": "string", "description": "field that results should be sorted by", @@ -19970,6 +19985,12 @@ func init() { "name": "originDutyLocation", "in": "query" }, + { + "type": "string", + "description": "Used to illustrate which user is assigned to this payment request.\n", + "name": "assignedTo", + "in": "query" + }, { "uniqueItems": true, "type": "array", @@ -27705,6 +27726,13 @@ func init() { "type": "number", "format": "double" }, + "assignable": { + "type": "boolean" + }, + "assignedTo": { + "x-nullable": true, + "$ref": "#/definitions/AssignedOfficeUser" + }, "availableOfficeUsers": { "$ref": "#/definitions/AvailableOfficeUsers" }, diff --git a/pkg/gen/ghcapi/ghcoperations/queues/get_payment_requests_queue_parameters.go b/pkg/gen/ghcapi/ghcoperations/queues/get_payment_requests_queue_parameters.go index dd375958129..a0b33924190 100644 --- a/pkg/gen/ghcapi/ghcoperations/queues/get_payment_requests_queue_parameters.go +++ b/pkg/gen/ghcapi/ghcoperations/queues/get_payment_requests_queue_parameters.go @@ -34,6 +34,11 @@ type GetPaymentRequestsQueueParams struct { // HTTP Request Object HTTPRequest *http.Request `json:"-"` + /*Used to illustrate which user is assigned to this payment request. + + In: query + */ + AssignedTo *string /* In: query */ @@ -109,6 +114,11 @@ func (o *GetPaymentRequestsQueueParams) BindRequest(r *http.Request, route *midd qs := runtime.Values(r.URL.Query()) + qAssignedTo, qhkAssignedTo, _ := qs.GetOK("assignedTo") + if err := o.bindAssignedTo(qAssignedTo, qhkAssignedTo, route.Formats); err != nil { + res = append(res, err) + } + qBranch, qhkBranch, _ := qs.GetOK("branch") if err := o.bindBranch(qBranch, qhkBranch, route.Formats); err != nil { res = append(res, err) @@ -189,6 +199,24 @@ func (o *GetPaymentRequestsQueueParams) BindRequest(r *http.Request, route *midd return nil } +// bindAssignedTo binds and validates parameter AssignedTo from query. +func (o *GetPaymentRequestsQueueParams) bindAssignedTo(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.AssignedTo = &raw + + return nil +} + // bindBranch binds and validates parameter Branch from query. func (o *GetPaymentRequestsQueueParams) bindBranch(rawData []string, hasKey bool, formats strfmt.Registry) error { var raw string @@ -436,7 +464,7 @@ func (o *GetPaymentRequestsQueueParams) bindSort(rawData []string, hasKey bool, // validateSort carries on validations for parameter Sort func (o *GetPaymentRequestsQueueParams) validateSort(formats strfmt.Registry) error { - if err := validate.EnumCase("sort", "query", *o.Sort, []interface{}{"lastName", "locator", "submittedAt", "branch", "status", "dodID", "emplid", "age", "originDutyLocation"}, true); err != nil { + if err := validate.EnumCase("sort", "query", *o.Sort, []interface{}{"lastName", "locator", "submittedAt", "branch", "status", "dodID", "emplid", "age", "originDutyLocation", "assignedTo"}, true); err != nil { return err } diff --git a/pkg/gen/ghcapi/ghcoperations/queues/get_payment_requests_queue_urlbuilder.go b/pkg/gen/ghcapi/ghcoperations/queues/get_payment_requests_queue_urlbuilder.go index bf2dddde0bf..0c822e5d330 100644 --- a/pkg/gen/ghcapi/ghcoperations/queues/get_payment_requests_queue_urlbuilder.go +++ b/pkg/gen/ghcapi/ghcoperations/queues/get_payment_requests_queue_urlbuilder.go @@ -16,6 +16,7 @@ import ( // GetPaymentRequestsQueueURL generates an URL for the get payment requests queue operation type GetPaymentRequestsQueueURL struct { + AssignedTo *string Branch *string DestinationDutyLocation *string DodID *string @@ -66,6 +67,14 @@ func (o *GetPaymentRequestsQueueURL) Build() (*url.URL, error) { qs := make(url.Values) + var assignedToQ string + if o.AssignedTo != nil { + assignedToQ = *o.AssignedTo + } + if assignedToQ != "" { + qs.Set("assignedTo", assignedToQ) + } + var branchQ string if o.Branch != nil { branchQ = *o.Branch diff --git a/pkg/gen/ghcmessages/queue_payment_request.go b/pkg/gen/ghcmessages/queue_payment_request.go index 737d1198f5d..6f173d37b76 100644 --- a/pkg/gen/ghcmessages/queue_payment_request.go +++ b/pkg/gen/ghcmessages/queue_payment_request.go @@ -22,6 +22,12 @@ type QueuePaymentRequest struct { // Days since the payment request has been requested. Decimal representation will allow more accurate sorting. Age float64 `json:"age,omitempty"` + // assignable + Assignable bool `json:"assignable,omitempty"` + + // assigned to + AssignedTo *AssignedOfficeUser `json:"assignedTo,omitempty"` + // available office users AvailableOfficeUsers AvailableOfficeUsers `json:"availableOfficeUsers,omitempty"` @@ -71,6 +77,10 @@ type QueuePaymentRequest struct { func (m *QueuePaymentRequest) Validate(formats strfmt.Registry) error { var res []error + if err := m.validateAssignedTo(formats); err != nil { + res = append(res, err) + } + if err := m.validateAvailableOfficeUsers(formats); err != nil { res = append(res, err) } @@ -121,6 +131,25 @@ func (m *QueuePaymentRequest) Validate(formats strfmt.Registry) error { return nil } +func (m *QueuePaymentRequest) validateAssignedTo(formats strfmt.Registry) error { + if swag.IsZero(m.AssignedTo) { // not required + return nil + } + + if m.AssignedTo != nil { + if err := m.AssignedTo.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("assignedTo") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("assignedTo") + } + return err + } + } + + return nil +} + func (m *QueuePaymentRequest) validateAvailableOfficeUsers(formats strfmt.Registry) error { if swag.IsZero(m.AvailableOfficeUsers) { // not required return nil @@ -293,6 +322,10 @@ func (m *QueuePaymentRequest) validateSubmittedAt(formats strfmt.Registry) error func (m *QueuePaymentRequest) ContextValidate(ctx context.Context, formats strfmt.Registry) error { var res []error + if err := m.contextValidateAssignedTo(ctx, formats); err != nil { + res = append(res, err) + } + if err := m.contextValidateAvailableOfficeUsers(ctx, formats); err != nil { res = append(res, err) } @@ -323,6 +356,27 @@ func (m *QueuePaymentRequest) ContextValidate(ctx context.Context, formats strfm return nil } +func (m *QueuePaymentRequest) contextValidateAssignedTo(ctx context.Context, formats strfmt.Registry) error { + + if m.AssignedTo != nil { + + if swag.IsZero(m.AssignedTo) { // not required + return nil + } + + if err := m.AssignedTo.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("assignedTo") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("assignedTo") + } + return err + } + } + + return nil +} + func (m *QueuePaymentRequest) contextValidateAvailableOfficeUsers(ctx context.Context, formats strfmt.Registry) error { if err := m.AvailableOfficeUsers.ContextValidate(ctx, formats); err != nil { diff --git a/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go b/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go index a04920aa888..415cf7f3d69 100644 --- a/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go +++ b/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go @@ -2259,7 +2259,7 @@ func queuePaymentRequestStatus(paymentRequest models.PaymentRequest) string { } // QueuePaymentRequests payload -func QueuePaymentRequests(paymentRequests *models.PaymentRequests, officeUsers []models.OfficeUser) *ghcmessages.QueuePaymentRequests { +func QueuePaymentRequests(paymentRequests *models.PaymentRequests, officeUsers []models.OfficeUser, officeUser models.OfficeUser, isSupervisor bool) *ghcmessages.QueuePaymentRequests { queuePaymentRequests := make(ghcmessages.QueuePaymentRequests, len(*paymentRequests)) for i, paymentRequest := range *paymentRequests { @@ -2283,7 +2283,31 @@ func QueuePaymentRequests(paymentRequests *models.PaymentRequests, officeUsers [ OrderType: (*string)(orders.OrdersType.Pointer()), LockedByOfficeUserID: handlers.FmtUUIDPtr(moveTaskOrder.LockedByOfficeUserID), LockExpiresAt: handlers.FmtDateTimePtr(moveTaskOrder.LockExpiresAt), - AvailableOfficeUsers: *QueueAvailableOfficeUsers(officeUsers), + } + + if paymentRequest.MoveTaskOrder.TIOAssignedUser != nil { + queuePaymentRequests[i].AssignedTo = AssignedOfficeUser(paymentRequest.MoveTaskOrder.TIOAssignedUser) + } + + isAssignable := false + if queuePaymentRequests[i].AssignedTo == nil { + isAssignable = true + } + + if isSupervisor { + isAssignable = true + } + + queuePaymentRequests[i].Assignable = isAssignable + + // only need to attach available office users if move is assignable + if queuePaymentRequests[i].Assignable { + availableOfficeUsers := officeUsers + if !isSupervisor { + availableOfficeUsers = models.OfficeUsers{officeUser} + } + + queuePaymentRequests[i].AvailableOfficeUsers = *QueueAvailableOfficeUsers(availableOfficeUsers) } if orders.DepartmentIndicator != nil { diff --git a/pkg/handlers/ghcapi/queues.go b/pkg/handlers/ghcapi/queues.go index 800d02caa40..5465b9a6cb3 100644 --- a/pkg/handlers/ghcapi/queues.go +++ b/pkg/handlers/ghcapi/queues.go @@ -248,6 +248,7 @@ func (h GetPaymentRequestsQueueHandler) Handle( Order: params.Order, OriginDutyLocation: params.OriginDutyLocation, OrderType: params.OrderType, + TIOAssignedUser: params.AssignedTo, } listPaymentRequestParams.Status = []string{string(models.QueuePaymentRequestPaymentRequested)} @@ -319,7 +320,13 @@ func (h GetPaymentRequestsQueueHandler) Handle( } } - queuePaymentRequests := payloads.QueuePaymentRequests(paymentRequests, officeUsers) + privileges, err := models.FetchPrivilegesForUser(appCtx.DB(), appCtx.Session().UserID) + if err != nil { + appCtx.Logger().Error("Error retreiving user privileges", zap.Error(err)) + } + + isSupervisor := privileges.HasPrivilege(models.PrivilegeTypeSupervisor) + queuePaymentRequests := payloads.QueuePaymentRequests(paymentRequests, officeUsers, officeUser, isSupervisor) result := &ghcmessages.QueuePaymentRequestsResult{ TotalCount: int64(count), diff --git a/pkg/services/order/order_fetcher.go b/pkg/services/order/order_fetcher.go index 5e9ae7b8435..4155c4353c4 100644 --- a/pkg/services/order/order_fetcher.go +++ b/pkg/services/order/order_fetcher.go @@ -116,12 +116,12 @@ func (f orderFetcher) ListOrders(appCtx appcontext.AppContext, officeUserID uuid closeoutLocationQuery := closeoutLocationFilter(params.CloseoutLocation, ppmCloseoutGblocs) ppmTypeQuery := ppmTypeFilter(params.PPMType) ppmStatusQuery := ppmStatusFilter(params.PPMStatus) - SCAssignedUserQuery := SCAssignedUserFilter(params.SCAssignedUser) - TOOAssignedUserQuery := TOOAssignedUserFilter(params.TOOAssignedUser) + scAssignedUserQuery := scAssignedUserFilter(params.SCAssignedUser) + tooAssignedUserQuery := tooAssignedUserFilter(params.TOOAssignedUser) sortOrderQuery := sortOrder(params.Sort, params.Order, ppmCloseoutGblocs) counselingQuery := counselingOfficeFilter(params.CounselingOffice) // Adding to an array so we can iterate over them and apply the filters after the query structure is set below - options := [20]QueryOption{branchQuery, locatorQuery, dodIDQuery, emplidQuery, lastNameQuery, originDutyLocationQuery, destinationDutyLocationQuery, moveStatusQuery, gblocQuery, submittedAtQuery, appearedInTOOAtQuery, requestedMoveDateQuery, ppmTypeQuery, closeoutInitiatedQuery, closeoutLocationQuery, ppmStatusQuery, sortOrderQuery, SCAssignedUserQuery, TOOAssignedUserQuery, counselingQuery} + options := [20]QueryOption{branchQuery, locatorQuery, dodIDQuery, emplidQuery, lastNameQuery, originDutyLocationQuery, destinationDutyLocationQuery, moveStatusQuery, gblocQuery, submittedAtQuery, appearedInTOOAtQuery, requestedMoveDateQuery, ppmTypeQuery, closeoutInitiatedQuery, closeoutLocationQuery, ppmStatusQuery, sortOrderQuery, scAssignedUserQuery, tooAssignedUserQuery, counselingQuery} var query *pop.Query if ppmCloseoutGblocs { @@ -635,7 +635,7 @@ func ppmStatusFilter(ppmStatus *string) QueryOption { } } -func SCAssignedUserFilter(scAssigned *string) QueryOption { +func scAssignedUserFilter(scAssigned *string) QueryOption { return func(query *pop.Query) { if scAssigned != nil { nameSearch := fmt.Sprintf("%s%%", *scAssigned) @@ -644,7 +644,7 @@ func SCAssignedUserFilter(scAssigned *string) QueryOption { } } -func TOOAssignedUserFilter(tooAssigned *string) QueryOption { +func tooAssignedUserFilter(tooAssigned *string) QueryOption { return func(query *pop.Query) { if tooAssigned != nil { nameSearch := fmt.Sprintf("%s%%", *tooAssigned) diff --git a/pkg/services/payment_request.go b/pkg/services/payment_request.go index 25f62be43a4..827a02d21dd 100644 --- a/pkg/services/payment_request.go +++ b/pkg/services/payment_request.go @@ -93,6 +93,7 @@ type FetchPaymentRequestListParams struct { OriginDutyLocation *string OrderType *string ViewAsGBLOC *string + TIOAssignedUser *string } // ShipmentPaymentSITBalance is a public struct that's used to return current SIT balances to the TIO for a payment diff --git a/pkg/services/payment_request/payment_request_list_fetcher.go b/pkg/services/payment_request/payment_request_list_fetcher.go index 6071c11a9cc..334c49898fd 100644 --- a/pkg/services/payment_request/payment_request_list_fetcher.go +++ b/pkg/services/payment_request/payment_request_list_fetcher.go @@ -28,6 +28,7 @@ var parameters = map[string]string{ "status": "payment_requests.status", "age": "payment_requests.created_at", "originDutyLocation": "duty_locations.name", + "assignedTo": "assigned_user.last_name,assigned_user.first_name", } // NewPaymentRequestListFetcher returns a new payment request list fetcher @@ -61,6 +62,7 @@ func (f *paymentRequestListFetcher) FetchPaymentRequestList(appCtx appcontext.Ap query := appCtx.DB().Q().EagerPreload( "MoveTaskOrder.Orders.OriginDutyLocation.TransportationOffice", "MoveTaskOrder.Orders.OriginDutyLocation.Address", + "MoveTaskOrder.TIOAssignedUser", // See note further below about having to do this in a separate Load call due to a Pop issue. // "MoveTaskOrder.Orders.ServiceMember", ). @@ -73,6 +75,7 @@ func (f *paymentRequestListFetcher) FetchPaymentRequestList(appCtx appcontext.Ap // If a customer puts in an invalid ZIP for their pickup address, it won't show up in this view, // and we don't want it to get hidden from services counselors. LeftJoin("move_to_gbloc", "move_to_gbloc.move_id = moves.id"). + LeftJoin("office_users as assigned_user", "moves.tio_assigned_id = assigned_user.id"). Where("moves.show = ?", models.BoolPointer(true)) if !privileges.HasPrivilege(models.PrivilegeTypeSafety) { @@ -98,8 +101,9 @@ func (f *paymentRequestListFetcher) FetchPaymentRequestList(appCtx appcontext.Ap submittedAtQuery := submittedAtFilter(params.SubmittedAt) originDutyLocationQuery := dutyLocationFilter(params.OriginDutyLocation) orderQuery := sortOrder(params.Sort, params.Order) + tioAssignedUserQuery := tioAssignedUserFilter(params.TIOAssignedUser) - options := [11]QueryOption{branchQuery, locatorQuery, dodIDQuery, lastNameQuery, dutyLocationQuery, statusQuery, originDutyLocationQuery, submittedAtQuery, gblocQuery, orderQuery, emplidQuery} + options := [12]QueryOption{branchQuery, locatorQuery, dodIDQuery, lastNameQuery, dutyLocationQuery, statusQuery, originDutyLocationQuery, submittedAtQuery, gblocQuery, orderQuery, emplidQuery, tioAssignedUserQuery} for _, option := range options { if option != nil { @@ -115,7 +119,7 @@ func (f *paymentRequestListFetcher) FetchPaymentRequestList(appCtx appcontext.Ap params.PerPage = models.Int64Pointer(20) } - err = query.GroupBy("payment_requests.id, service_members.id, moves.id, duty_locations.id, duty_locations.name").Paginate(int(*params.Page), int(*params.PerPage)).All(&paymentRequests) + err = query.GroupBy("payment_requests.id, service_members.id, moves.id, duty_locations.id, duty_locations.name, assigned_user.last_name, assigned_user.first_name").Paginate(int(*params.Page), int(*params.PerPage)).All(&paymentRequests) if err != nil { return nil, 0, err } @@ -269,6 +273,11 @@ func orderName(query *pop.Query, order *string) *pop.Query { return query } +func orderAssignedName(query *pop.Query, order *string) *pop.Query { + query.Order(fmt.Sprintf("assigned_user.last_name %s, assigned_user.first_name %s", *order, *order)) + return query +} + func reverseOrder(order *string) string { if *order == "asc" { return "desc" @@ -284,6 +293,8 @@ func sortOrder(sort *string, order *string) QueryOption { orderName(query, order) } else if *sort == "age" { query.Order(fmt.Sprintf("%s %s", sortTerm, reverseOrder(order))) + } else if *sort == "assignedTo" { + orderAssignedName(query, order) } else { query.Order(fmt.Sprintf("%s %s", sortTerm, *order)) } @@ -401,3 +412,12 @@ func paymentRequestsStatusFilter(statuses []string) QueryOption { } } + +func tioAssignedUserFilter(tioAssigned *string) QueryOption { + return func(query *pop.Query) { + if tioAssigned != nil { + nameSearch := fmt.Sprintf("%s%%", *tioAssigned) + query.Where("assigned_user.last_name ILIKE ?", nameSearch) + } + } +} diff --git a/src/pages/Office/PaymentRequestQueue/PaymentRequestQueue.jsx b/src/pages/Office/PaymentRequestQueue/PaymentRequestQueue.jsx index 6179ed0a0f1..ddfab8f55d0 100644 --- a/src/pages/Office/PaymentRequestQueue/PaymentRequestQueue.jsx +++ b/src/pages/Office/PaymentRequestQueue/PaymentRequestQueue.jsx @@ -1,5 +1,6 @@ import React, { useCallback, useEffect, useState } from 'react'; import { useNavigate, NavLink, useParams, Navigate } from 'react-router-dom'; +import { Dropdown } from '@trussworks/react-uswds'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import styles from './PaymentRequestQueue.module.scss'; @@ -28,117 +29,159 @@ import { roleTypes } from 'constants/userRoles'; import { isNullUndefinedOrWhitespace } from 'shared/utils'; import NotFound from 'components/NotFound/NotFound'; import { isBooleanFlagEnabled } from 'utils/featureFlags'; -import { PAYMENT_REQUEST_STATUS } from 'shared/constants'; +import { DEFAULT_EMPTY_VALUE, PAYMENT_REQUEST_STATUS } from 'shared/constants'; +import handleQueueAssignment from 'utils/queues'; -export const columns = (moveLockFlag, showBranchFilter = true) => [ - createHeader( - ' ', - (row) => { - const now = new Date(); - // this will render a lock icon if the move is locked & if the lockExpiresAt value is after right now - if (row.lockedByOfficeUserID && row.lockExpiresAt && now < new Date(row.lockExpiresAt) && moveLockFlag) { +export const columns = (moveLockFlag, isQueueManagementEnabled, showBranchFilter = true) => { + const cols = [ + createHeader( + ' ', + (row) => { + const now = new Date(); + // this will render a lock icon if the move is locked & if the lockExpiresAt value is after right now + if (row.lockedByOfficeUserID && row.lockExpiresAt && now < new Date(row.lockExpiresAt) && moveLockFlag) { + return ( +