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 ( +
+ +
+ ); + } + return null; + }, + { + id: 'lock', + }, + ), + createHeader('ID', 'id', { id: 'id' }), + createHeader( + 'Customer name', + (row) => { return ( -
- +
+ {CHECK_SPECIAL_ORDERS_TYPES(row.orderType) ? ( + {SPECIAL_ORDERS_TYPES[`${row.orderType}`]} + ) : null} + {`${row.customer.last_name}, ${row.customer.first_name}`}
); - } - return null; - }, - { - id: 'lock', - }, - ), - createHeader('ID', 'id', { id: 'id' }), - createHeader( - 'Customer name', - (row) => { - return ( -
- {CHECK_SPECIAL_ORDERS_TYPES(row.orderType) ? ( - {SPECIAL_ORDERS_TYPES[`${row.orderType}`]} - ) : null} - {`${row.customer.last_name}, ${row.customer.first_name}`} -
- ); - }, - { - id: 'lastName', + }, + { + id: 'lastName', + isFilterable: true, + exportValue: (row) => { + return `${row.customer.last_name}, ${row.customer.first_name}`; + }, + }, + ), + createHeader('DoD ID', 'customer.dodID', { + id: 'dodID', isFilterable: true, exportValue: (row) => { - return `${row.customer.last_name}, ${row.customer.first_name}`; + return row.customer.dodID; }, - }, - ), - createHeader('DoD ID', 'customer.dodID', { - id: 'dodID', - isFilterable: true, - exportValue: (row) => { - return row.customer.dodID; - }, - }), - createHeader('EMPLID', 'customer.emplid', { - id: 'emplid', - isFilterable: true, - }), - createHeader( - 'Status', - (row) => { - return row.status !== PAYMENT_REQUEST_STATUS.PAYMENT_REQUESTED ? paymentRequestStatusReadable(row.status) : null; - }, - { - id: 'status', - disableSortBy: true, - }, - ), - createHeader( - 'Age', - (row) => { - return formatAgeToDays(row.age); - }, - { id: 'age' }, - ), - createHeader( - 'Submitted', - (row) => { - return formatDateFromIso(row.submittedAt, 'DD MMM YYYY'); - }, - { - id: 'submittedAt', + }), + createHeader('EMPLID', 'customer.emplid', { + id: 'emplid', isFilterable: true, - // eslint-disable-next-line react/jsx-props-no-spreading - Filter: (props) => , - }, - ), - createHeader('Move Code', 'locator', { - id: 'locator', - isFilterable: true, - }), - createHeader( - 'Branch', - (row) => { - return serviceMemberAgencyLabel(row.customer.agency); - }, - { - id: 'branch', - isFilterable: showBranchFilter, - Filter: (props) => ( + }), + createHeader( + 'Status', + (row) => { + return row.status !== PAYMENT_REQUEST_STATUS.PAYMENT_REQUESTED + ? paymentRequestStatusReadable(row.status) + : null; + }, + { + id: 'status', + disableSortBy: true, + }, + ), + createHeader( + 'Age', + (row) => { + return formatAgeToDays(row.age); + }, + { id: 'age' }, + ), + createHeader( + 'Submitted', + (row) => { + return formatDateFromIso(row.submittedAt, 'DD MMM YYYY'); + }, + { + id: 'submittedAt', + isFilterable: true, // eslint-disable-next-line react/jsx-props-no-spreading - + Filter: (props) => , + }, + ), + createHeader('Move Code', 'locator', { + id: 'locator', + isFilterable: true, + }), + createHeader( + 'Branch', + (row) => { + return serviceMemberAgencyLabel(row.customer.agency); + }, + { + id: 'branch', + isFilterable: showBranchFilter, + Filter: (props) => ( + // eslint-disable-next-line react/jsx-props-no-spreading + + ), + }, + ), + createHeader('Origin GBLOC', 'originGBLOC', { disableSortBy: true }), + createHeader('Origin Duty Location', 'originDutyLocation.name', { + id: 'originDutyLocation', + isFilterable: true, + exportValue: (row) => { + return row.originDutyLocation?.name; + }, + }), + ]; + + if (isQueueManagementEnabled) + cols.push( + createHeader( + 'Assigned', + (row) => { + return !row?.assignable ? ( +
{`${row.assignedTo?.lastName}, ${row.assignedTo?.firstName}`}
+ ) : ( +
+ { + handleQueueAssignment(row.moveID, e.target.value, roleTypes.TIO); + }} + title="Assigned dropdown" + > + + {row.availableOfficeUsers.map(({ lastName, firstName, officeUserId }) => { + return ( + + ); + })} + +
+ ); + }, + { + id: 'assignedTo', + isFilterable: true, + }, ), - }, - ), - createHeader('Origin GBLOC', 'originGBLOC', { disableSortBy: true }), - createHeader('Origin Duty Location', 'originDutyLocation.name', { - id: 'originDutyLocation', - isFilterable: true, - exportValue: (row) => { - return row.originDutyLocation?.name; - }, - }), -]; + ); + + return cols; +}; -const PaymentRequestQueue = () => { +const PaymentRequestQueue = ({ isQueueManagementFFEnabled }) => { const { queueType } = useParams(); const navigate = useNavigate(); const [search, setSearch] = useState({ moveCode: null, dodID: null, customerName: null, paymentRequestCode: null }); @@ -185,8 +228,14 @@ const PaymentRequestQueue = () => { // eslint-disable-next-line camelcase const showBranchFilter = office_user?.transportation_office?.gbloc !== GBLOC.USMC; - const handleClick = (values) => { - navigate(`/moves/${values.locator}/payment-requests`); + const handleClick = (values, e) => { + const assignedSelect = e.target.closest('div[data-label="assignedSelect"]'); + + if (assignedSelect) { + // do nothing when clicking on the assignedSelect column + } else { + navigate(`/moves/${values.locator}/payment-requests`); + } }; if (isLoading) return ; @@ -260,7 +309,7 @@ const PaymentRequestQueue = () => { defaultSortedColumns={[{ id: 'age', desc: true }]} disableMultiSort disableSortBy={false} - columns={columns(moveLockFlag, showBranchFilter)} + columns={columns(moveLockFlag, isQueueManagementFFEnabled, showBranchFilter)} title="Payment requests" handleClick={handleClick} useQueries={usePaymentRequestQueueQueries} diff --git a/src/pages/Office/PaymentRequestQueue/PaymentRequestQueue.module.scss b/src/pages/Office/PaymentRequestQueue/PaymentRequestQueue.module.scss index 8aeef2d9a11..130ed0ae499 100644 --- a/src/pages/Office/PaymentRequestQueue/PaymentRequestQueue.module.scss +++ b/src/pages/Office/PaymentRequestQueue/PaymentRequestQueue.module.scss @@ -19,3 +19,7 @@ display: block; color: red; } + +.assignedToCol { + width: 10vw; +} diff --git a/src/pages/Office/PaymentRequestQueue/PaymentRequestQueue.test.jsx b/src/pages/Office/PaymentRequestQueue/PaymentRequestQueue.test.jsx index f86a76369ee..19ec5edea22 100644 --- a/src/pages/Office/PaymentRequestQueue/PaymentRequestQueue.test.jsx +++ b/src/pages/Office/PaymentRequestQueue/PaymentRequestQueue.test.jsx @@ -37,6 +37,11 @@ jest.mock('hooks/queries', () => ({ data: [ { age: 0.8477863, + assignedTo: { + firstName: 'Alice', + lastName: 'Bob', + officeUserId: '404011d1-052a-4c34-b2e0-71dd5082718a', + }, customer: { agency: 'COAST_GUARD', dodID: '3305957632', @@ -62,8 +67,47 @@ jest.mock('hooks/queries', () => ({ lockExpiresAt: '2099-10-15T23:48:35.420Z', lockedByOfficeUserID: '2744435d-7ba8-4cc5-bae5-f302c72c966e', }, + { + age: 0.8477863, + availableOfficeUsers: [ + { + firstName: 'Alice', + lastName: 'Bob', + officeUserId: '404011d1-052a-4c34-b2e0-71dd5082718a', + }, + ], + customer: { + agency: 'NAVY', + cacValidated: true, + eTag: 'MjAyNC0xMC0xMFQyMjoyNDo1My4xNjYxNjNa', + dodID: '1234567890', + edipi: '1234567', + email: '20241010222310-ae019978709c@example.com', + first_name: 'Ooga', + id: 'f23b5293-8ef0-453d-bd51-7c21d9730fcb', + last_name: 'Booga', + middle_name: '', + phone: '211-111-1111', + phoneIsPreferred: true, + secondaryTelephone: '', + suffix: '', + userID: 'e9d421af-b598-4ff1-b102-d8b14d414129', + }, + departmentIndicator: 'COAST_GUARD', + id: '32b90458-2171-4451-bb0a-c5c0db897e34', + locator: '0OOGAB', + moveID: '8f29e53d-e816-4476-bfee-f38d07b94f2d', + originGBLOC: 'LKNQ', + status: 'PENDING', + submittedAt: '2020-10-17T23:48:35.420Z', + originDutyLocation: { + name: 'Scott AFB', + }, + lockExpiresAt: '2099-10-15T23:48:35.420Z', + lockedByOfficeUserID: '2744435d-7ba8-4cc5-bae5-f302c72c966e', + }, ], - totalCount: 1, + totalCount: 2, }, isLoading: false, isError: false, @@ -121,26 +165,62 @@ describe('PaymentRequestQueue', () => { , ); - expect(screen.queryByText('Payment requests (1)')).toBeInTheDocument(); + expect(screen.queryByText('Payment requests (2)')).toBeInTheDocument(); }); - it('renders the table with data and expected values', () => { + it('renders the table with data and expected values without queue management ff', async () => { reactRouterDom.useParams.mockReturnValue({ queueType: tioRoutes.PAYMENT_REQUEST_QUEUE }); + const wrapper = mount( + + {/* eslint-disable-next-line react/jsx-props-no-spreading */} + + , + ); render( , ); - expect(screen.getByRole('cell', { name: 'Spacemen, Leo' })).toBeInTheDocument(); - expect(screen.getByRole('cell', { name: '3305957632' })).toBeInTheDocument(); - expect(screen.getByRole('cell', { name: '1253694' })).toBeInTheDocument(); - expect(screen.getByRole('cell', { name: 'Payment requested' })).toBeInTheDocument(); - expect(screen.getByRole('cell', { name: 'Less than 1 day' })).toBeInTheDocument(); - expect(screen.getByRole('cell', { name: '15 Oct 2020' })).toBeInTheDocument(); - expect(screen.getByRole('cell', { name: 'R993T7' })).toBeInTheDocument(); - expect(screen.getByRole('cell', { name: 'Coast Guard' })).toBeInTheDocument(); - expect(screen.getByRole('cell', { name: 'LKNQ' })).toBeInTheDocument(); - expect(screen.getByRole('cell', { name: 'Scott AFB' })).toBeInTheDocument(); + + const paymentRequests = wrapper.find('tbody tr'); + const firstPaymentRequest = paymentRequests.at(0); + + expect(firstPaymentRequest.find('td.lastName').text()).toBe('Spacemen, Leo'); + expect(firstPaymentRequest.find('td.dodID').text()).toBe('3305957632'); + expect(firstPaymentRequest.find('td.emplid').text()).toBe('1253694'); + expect(firstPaymentRequest.find('td.status').text()).toBe('Payment requested'); + expect(firstPaymentRequest.find('td.age').text()).toBe('Less than 1 day'); + expect(firstPaymentRequest.find('td.submittedAt').text()).toBe('15 Oct 2020'); + expect(firstPaymentRequest.find('td.locator').text()).toBe('R993T7'); + expect(firstPaymentRequest.find('td.branch').text()).toBe('Coast Guard'); + expect(firstPaymentRequest.find('td.originGBLOC').text()).toBe('LKNQ'); + expect(firstPaymentRequest.find('td.originDutyLocation').text()).toBe('Scott AFB'); + + const secondPaymentRequest = paymentRequests.at(1); + expect(secondPaymentRequest.find('td.lastName').text()).toBe('Booga, Ooga'); + expect(secondPaymentRequest.find('td.dodID').text()).toBe('1234567890'); + expect(secondPaymentRequest.find('td.emplid').text()).toBe(''); + expect(secondPaymentRequest.find('td.status').text()).toBe('Payment requested'); + expect(secondPaymentRequest.find('td.age').text()).toBe('Less than 1 day'); + expect(secondPaymentRequest.find('td.submittedAt').text()).toBe('17 Oct 2020'); + expect(secondPaymentRequest.find('td.locator').text()).toBe('0OOGAB'); + expect(secondPaymentRequest.find('td.branch').text()).toBe('Navy'); + expect(secondPaymentRequest.find('td.originGBLOC').text()).toBe('LKNQ'); + expect(secondPaymentRequest.find('td.originDutyLocation').text()).toBe('Scott AFB'); + }); + + it('renders the table with data and expected values with queue management ff', async () => { + reactRouterDom.useParams.mockReturnValue({ queueType: tioRoutes.PAYMENT_REQUEST_QUEUE }); + render( + + + , + ); + + await waitFor(() => { + const assignedSelect = screen.queryAllByTestId('assigned-col')[0]; + expect(assignedSelect).toBeInTheDocument(); + }); }); it('applies the sort to the age column in descending direction', () => { @@ -336,7 +416,7 @@ describe('PaymentRequestQueue', () => { , ); // expect Payment requested status to appear in the TIO queue - expect(screen.queryByText('Payment requested')).toBeInTheDocument(); + expect(screen.getAllByText('Payment requested')).toHaveLength(2); // expect other statuses NOT to appear in the TIO queue expect(screen.queryByText('Deprecated')).not.toBeInTheDocument(); expect(screen.queryByText('Error')).not.toBeInTheDocument(); @@ -363,7 +443,7 @@ describe('PaymentRequestQueue', () => { , ); await waitFor(() => { - const lockIcon = screen.queryByTestId('lock-icon'); + const lockIcon = screen.queryAllByTestId('lock-icon')[0]; expect(lockIcon).toBeInTheDocument(); }); }); @@ -381,4 +461,30 @@ describe('PaymentRequestQueue', () => { expect(lockIcon).not.toBeInTheDocument(); }); }); + it('renders assignedTo columns when the queue management flag is on', async () => { + reactRouterDom.useParams.mockReturnValue({ queueType: tioRoutes.PAYMENT_REQUEST_QUEUE }); + + render( + + + , + ); + await waitFor(() => { + const assignedToColumn = screen.queryByText('Assigned'); + expect(assignedToColumn).toBeInTheDocument(); + }); + }); + it('does NOT render assignedTo columns when the queue management flag is on', async () => { + reactRouterDom.useParams.mockReturnValue({ queueType: tioRoutes.PAYMENT_REQUEST_QUEUE }); + + render( + + + , + ); + await waitFor(() => { + const assignedToColumn = screen.queryByText('Assigned'); + expect(assignedToColumn).not.toBeInTheDocument(); + }); + }); }); diff --git a/src/pages/Office/index.jsx b/src/pages/Office/index.jsx index e6e14b521ef..65d51d03671 100644 --- a/src/pages/Office/index.jsx +++ b/src/pages/Office/index.jsx @@ -282,7 +282,7 @@ export class OfficeApp extends Component { path="/invoicing/queue" element={ - + } /> @@ -340,7 +340,7 @@ export class OfficeApp extends Component { end element={ - + } /> @@ -572,7 +572,13 @@ export class OfficeApp extends Component { } /> {/* ROOT */} - {activeRole === roleTypes.TIO && } />} + {activeRole === roleTypes.TIO && ( + } + /> + )} {activeRole === roleTypes.TOO && } />} {activeRole === roleTypes.HQ && !hqRoleFlag && ( } /> diff --git a/swagger-def/ghc.yaml b/swagger-def/ghc.yaml index 5dddb4f87bc..10a610a0168 100644 --- a/swagger-def/ghc.yaml +++ b/swagger-def/ghc.yaml @@ -3581,7 +3581,7 @@ paths: - in: query name: sort type: string - enum: [lastName, locator, submittedAt, branch, status, dodID, emplid, age, originDutyLocation] + enum: [lastName, locator, submittedAt, branch, status, dodID, emplid, age, originDutyLocation, assignedTo] description: field that results should be sorted by - in: query name: order @@ -3622,6 +3622,11 @@ paths: - in: query name: originDutyLocation type: string + - in: query + name: assignedTo + type: string + description: | + Used to illustrate which user is assigned to this payment request. - in: query name: status type: array @@ -6928,8 +6933,13 @@ definitions: type: string format: date-time x-nullable: true + assignedTo: + $ref: '#/definitions/AssignedOfficeUser' + x-nullable: true availableOfficeUsers: $ref: '#/definitions/AvailableOfficeUsers' + assignable: + type: boolean QueuePaymentRequests: type: array items: diff --git a/swagger/ghc.yaml b/swagger/ghc.yaml index 37d14b07416..2f9350b5897 100644 --- a/swagger/ghc.yaml +++ b/swagger/ghc.yaml @@ -3746,6 +3746,7 @@ paths: - emplid - age - originDutyLocation + - assignedTo description: field that results should be sorted by - in: query name: order @@ -3790,6 +3791,11 @@ paths: - in: query name: originDutyLocation type: string + - in: query + name: assignedTo + type: string + description: | + Used to illustrate which user is assigned to this payment request. - in: query name: status type: array @@ -7225,8 +7231,13 @@ definitions: type: string format: date-time x-nullable: true + assignedTo: + $ref: '#/definitions/AssignedOfficeUser' + x-nullable: true availableOfficeUsers: $ref: '#/definitions/AvailableOfficeUsers' + assignable: + type: boolean QueuePaymentRequests: type: array items: