Skip to content

Commit 4acf015

Browse files
josbellclaude
andcommitted
fix(pre-award): enable re-request after approval declined
Previously when a Division Director declined a pre-award approval request, the "Request Pre-Award Approval" button remained disabled, preventing requesters from fixing issues and re-submitting. Changes: - Explicitly check for DECLINED status to allow re-requesting - Update button disable logic in both RequestPreAwardApproval and ProcurementTrackerStepFive - Reset approval_requested to false when declining (already implemented) - Add tests for declined status button enablement - Update mock data with DECLINED example and document handlers - Update Final Consensus Memo UI with disabled icon and tooltip - Enable MSW for local testing Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 5065e97 commit 4acf015

10 files changed

Lines changed: 164 additions & 20 deletions

File tree

frontend/src/components/Agreements/ProcurementTracker/ProcurementTrackerStepFive/ProcurementTrackerStepFive.jsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,10 @@ const ProcurementTrackerStepFive = ({
7777
const isUsersComboBoxDisabled = isDisabled || !isPreAwardComplete || authorizedUsers.length === 0;
7878
const isPreAwardFieldsDisabled = isDisabled || !isPreAwardComplete;
7979
const hasBLIInReview = budgetLineItems?.some((bli) => bli.in_review) ?? false;
80-
const isRequestBtnDisabled = isDisabled || !isActiveStep || !!stepFiveData?.approval_requested || hasBLIInReview;
80+
// Allow re-requesting when approval is declined, even if approval_requested was previously true
81+
const isApprovalDeclined = stepFiveData?.approval_status === "DECLINED";
82+
const isRequestBtnDisabled =
83+
isDisabled || !isActiveStep || (!!stepFiveData?.approval_requested && !isApprovalDeclined) || hasBLIInReview;
8184
const isStep5SubmitDisabled = Boolean(
8285
isDisabled ||
8386
!isPreAwardComplete ||

frontend/src/components/Agreements/ProcurementTracker/ProcurementTrackerStepFive/ProcurementTrackerStepFive.test.jsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,25 @@ describe("ProcurementTrackerStepFive", () => {
296296
expect(requestButton).toBeDisabled();
297297
});
298298

299+
it("renders Request Pre-Award Approval button as enabled when approval is declined", () => {
300+
render(
301+
<ProcurementTrackerStepFive
302+
stepStatus="PENDING"
303+
stepFiveData={{ ...mockStepData, approval_requested: false, approval_status: "DECLINED" }}
304+
authorizedUsers={mockAllUsers}
305+
isDisabled={false}
306+
isActiveStep={true}
307+
handleSetCompletedStepNumber={mockHandleSetCompletedStepNumber}
308+
agreementId={13}
309+
budgetLineItems={[]}
310+
/>
311+
);
312+
313+
const requestButton = screen.getByText("Request Pre-Award Approval");
314+
expect(requestButton).toBeInTheDocument();
315+
expect(requestButton).not.toBeDisabled();
316+
});
317+
299318
it("Target Completion Date has correct props", () => {
300319
render(
301320
<ProcurementTrackerStepFive

frontend/src/mocks/handlers.js

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ const procurementTrackers = [
151151
];
152152

153153
const procurementTrackerSteps = [
154-
// Tracker 1 - Agreement 9
154+
// Tracker 1 - Agreement 9 - Example of declined approval (can re-request)
155155
{
156156
id: 101,
157157
procurement_tracker_id: 1,
@@ -162,8 +162,13 @@ const procurementTrackerSteps = [
162162
status: "ACTIVE",
163163
step_start_date: "2024-01-20",
164164
step_completed_date: null,
165+
approval_requested: false, // Reset after decline
166+
approval_status: "DECLINED", // Status after decline
167+
approval_requested_date: "2024-01-22T10:00:00.000Z",
168+
requestor_notes: "Please review the updated budget lines",
169+
reviewer_notes: "Budget allocation needs adjustment before approval",
165170
created_on: "2024-01-15T10:00:00.000Z",
166-
updated_on: "2024-01-20T14:30:00.000Z"
171+
updated_on: "2024-01-23T14:30:00.000Z"
167172
},
168173
{
169174
id: 102,
@@ -466,4 +471,21 @@ export const userHandlers = [
466471
})
467472
];
468473

469-
export const handlers = [...authHandlers, ...procurementHandlers, ...userHandlers];
474+
export const documentHandlers = [
475+
http.get(`${BACKEND_DOMAIN}/api/v1/agreements/:id/documents`, () => {
476+
return HttpResponse.json({
477+
documents: [
478+
{
479+
id: 1,
480+
document_name: "Final_Consensus_Memo_2026.pdf",
481+
document_type: "PRE_AWARD_CONSENSUS_MEMO",
482+
document_size: "2.5",
483+
uploaded_by: 18,
484+
uploaded_date: "2026-04-01T10:30:00Z"
485+
}
486+
]
487+
});
488+
})
489+
];
490+
491+
export const handlers = [...authHandlers, ...procurementHandlers, ...userHandlers, ...documentHandlers];

frontend/src/pages/agreements/pre-award-approval/ApprovePreAwardApproval.hooks.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,12 +157,22 @@ export default function useApprovePreAwardApproval(agreementId) {
157157
setSubmitError("");
158158

159159
try {
160+
// When declining, reset approval_requested to allow re-requesting
161+
const updateData =
162+
action === "DECLINED"
163+
? {
164+
approval_status: action,
165+
reviewer_notes: reviewerNotes.trim() || null,
166+
approval_requested: false
167+
}
168+
: {
169+
approval_status: action,
170+
reviewer_notes: reviewerNotes.trim() || null
171+
};
172+
160173
await updateProcurementTrackerStep({
161174
stepId: step5.id,
162-
data: {
163-
approval_status: action,
164-
reviewer_notes: reviewerNotes.trim() || null
165-
}
175+
data: updateData
166176
}).unwrap();
167177

168178
// Show alert and navigate back to For Review tab

frontend/src/pages/agreements/pre-award-approval/ApprovePreAwardApproval.jsx

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -234,18 +234,58 @@ export const ApprovePreAwardApproval = () => {
234234
))
235235
) : (
236236
<div
237-
className="bg-base-lightest border-1px border-base-light padding-3 margin-top-3"
238-
style={{ maxWidth: "540px" }}
237+
className="margin-top-3"
238+
style={{ display: "flex", alignItems: "center", gap: "1rem" }}
239239
>
240-
<div style={{ fontSize: "0.875rem", color: "#757575", marginBottom: "0.5rem" }}>
241-
Final Consensus Memo
240+
<div
241+
className="border-1px border-base-light padding-2"
242+
style={{
243+
backgroundColor: "white",
244+
maxWidth: "460px",
245+
flexGrow: 1
246+
}}
247+
>
248+
<div
249+
style={{
250+
fontSize: "0.875rem",
251+
color: "#757575",
252+
marginBottom: "0.5rem"
253+
}}
254+
>
255+
Final Consensus Memo
256+
</div>
257+
<div style={{ display: "flex", justifyContent: "flex-end" }}>
258+
<button
259+
type="button"
260+
className="usa-button--unstyled"
261+
style={{ padding: "0.5rem", cursor: "not-allowed" }}
262+
title="Document upload coming soon"
263+
aria-label="Download document (disabled)"
264+
disabled
265+
>
266+
<svg
267+
className="usa-icon"
268+
aria-hidden="true"
269+
focusable="false"
270+
style={{ fill: "#757575", width: "24px", height: "24px" }}
271+
>
272+
<use href={`${icons}#file_download`}></use>
273+
</svg>
274+
</button>
275+
</div>
242276
</div>
243-
<p
244-
className="margin-0"
245-
style={{ fontSize: "0.875rem", color: "#757575" }}
277+
<div
278+
className="bg-base-dark padding-2"
279+
style={{
280+
color: "white",
281+
fontSize: "0.875rem",
282+
maxWidth: "420px",
283+
borderRadius: "4px"
284+
}}
246285
>
247-
No documents uploaded
248-
</p>
286+
Upload Documents is coming soon! For now, please review within the OPRE preferred tool to
287+
share documents
288+
</div>
249289
</div>
250290
)}
251291
</Accordion>

frontend/src/pages/agreements/pre-award-approval/ApprovePreAwardApproval.test.jsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,10 @@ describe("ApprovePreAwardApproval", () => {
298298
renderComponent({ ...mockHookData, preAwardMemoDocuments: [] });
299299

300300
expect(screen.getByTestId("accordion-review-final-consensus-memo")).toBeInTheDocument();
301-
expect(screen.getByText("No documents uploaded")).toBeInTheDocument();
301+
expect(
302+
screen.getByText(/Upload Documents is coming soon! For now, please review within the OPRE preferred tool/)
303+
).toBeInTheDocument();
304+
expect(screen.getByRole("button", { name: "Download document (disabled)" })).toBeDisabled();
302305
});
303306

304307
it("should not display submitter notes section when notes are empty", () => {

frontend/src/pages/agreements/pre-award-approval/RequestPreAwardApproval.hooks.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,11 @@ export default function useRequestPreAwardApproval(agreementId) {
8686
// Check if approval has been approved (prevents re-requesting)
8787
const isApprovalApproved = step5?.approval_status === "APPROVED";
8888

89-
// Disable editing when pending OR approved (but allow re-request when declined)
90-
const hasApprovalBeenRequested = isApprovalPending || isApprovalApproved;
89+
// Check if approval was declined (allows re-requesting)
90+
const isApprovalDeclined = step5?.approval_status === "DECLINED";
91+
92+
// Disable editing when pending OR approved, but allow re-request when declined
93+
const hasApprovalBeenRequested = (isApprovalPending || isApprovalApproved) && !isApprovalDeclined;
9194

9295
// Check if any BLI is in review status
9396
const hasBLIInReview = agreement?.budget_line_items?.some((/** @type {any} */ bli) => bli.in_review) ?? false;

frontend/src/pages/agreements/pre-award-approval/RequestPreAwardApproval.test.jsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,4 +284,31 @@ describe("RequestPreAwardApproval", () => {
284284
screen.getByText(/You must complete Step 4 \(Evaluation\) in the Procurement Tracker/)
285285
).toBeInTheDocument();
286286
});
287+
288+
it("enables submit button when approval is declined", () => {
289+
requestPreAwardApprovalHookMock.mockReturnValue({
290+
...baseHookResult(),
291+
hasApprovalBeenRequested: false, // Should be false when declined
292+
hasBLIInReview: false,
293+
isSubmitting: false,
294+
isStep4Completed: true
295+
});
296+
297+
render(<RequestPreAwardApproval />);
298+
299+
const submitButton = screen.getByRole("button", { name: "Request Pre-Award Approval" });
300+
expect(submitButton).not.toBeDisabled();
301+
});
302+
303+
it("disables submit button when approval is approved", () => {
304+
requestPreAwardApprovalHookMock.mockReturnValue({
305+
...baseHookResult(),
306+
hasApprovalBeenRequested: true
307+
});
308+
309+
render(<RequestPreAwardApproval />);
310+
311+
const submitButton = screen.getByRole("button", { name: "Request Pre-Award Approval" });
312+
expect(submitButton).toBeDisabled();
313+
});
287314
});

frontend/src/tests/mocks.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,22 @@ export const handlers = [
136136
limit: limit,
137137
offset: offset
138138
});
139+
}),
140+
141+
// Mock documents endpoint for pre-award approval
142+
http.get("https://localhost:8000/api/v1/agreements/:id/documents", () => {
143+
return HttpResponse.json({
144+
documents: [
145+
{
146+
id: 1,
147+
document_name: "Final_Consensus_Memo_2026.pdf",
148+
document_type: "PRE_AWARD_CONSENSUS_MEMO",
149+
document_size: "2.5",
150+
uploaded_by: 1,
151+
uploaded_date: "2026-04-01T10:30:00Z"
152+
}
153+
]
154+
});
139155
})
140156
];
141157

frontend/vite-env/env.dev.local

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ VITE_HHSAMS_CLIENT_ID=44fe2c7a-e9c5-43ec-87e9-3de78d2d3a11
22
VITE_HHSAMS_AUTH_ENDPOINT=https://sso-stage.acf.hhs.gov/auth/realms/ACF-SSO/protocol/openid-connect/auth
33
VITE_HHSAMS_LOGOUT_ENDPOINT=https://sso-stage.acf.hhs.gov/auth/realms/ACF-SSO/protocol/openid-connect/logout
44
VITE_AUTH_DEBUG=1
5+
VITE_ENABLE_MSW=true

0 commit comments

Comments
 (0)